- Implement session management API (/api/v1/session/create, verify, logout) - Add session restoration from localStorage on page reload - Fix session_id priority when loading drafts (use current, not old from DB) - Add unified_id and claim_id to wizard payload sent to n8n - Add Docker volume for frontend HMR (Hot Module Replacement) - Add comprehensive session logging for debugging Components updated: - backend/app/api/session.py (NEW) - Session management endpoints - backend/app/main.py - Include session router - frontend/src/components/form/Step1Phone.tsx v2.0 - Create session after SMS - frontend/src/pages/ClaimForm.tsx v3.8 - Session restoration & priority fix - frontend/src/components/form/StepWizardPlan.tsx v1.4 - Add unified_id/claim_id - docker-compose.yml - Add frontend volume for live reload Session flow: 1. User verifies phone -> session created in Redis (24h TTL) 2. session_token saved to localStorage 3. Page reload -> session restored automatically 4. Draft selected -> current session_id used (not old from DB) 5. Wizard submit -> unified_id, claim_id, session_id sent to n8n 6. Logout -> session removed from Redis & localStorage Fixes: - Session token not persisting after page reload - unified_id missing in n8n webhook payload - Old session_id from draft overwriting current session - Frontend changes requiring container rebuild
14 KiB
Исправление узла claimsave для сохранения первичного черновика
Проблемы
claim_idгенерируется в другом workflow - нужно использоватьsession_idдля связи,claim_idгенерировать позже- Неправильный
sessionTokenв Code4 - используетсяclaim_idвместоsession_token - Нет сохранения первичного черновика - нужно сохранить сразу после генерации
wizard_plan - Данные из AI Agent1 и AI Agent13 не сохраняются - они пригодятся, нужно их сохранить в черновик
Решение
1. Исправить узел Code4 (подготовка данных для Redis)
Текущий код (строка 459):
const sessionToken = $('Redis Trigger').first().json.message.claim_id
Проблема: Используется claim_id вместо session_token для Redis ключа. claim_id может быть недоступен или генерируется позже.
Исправленный код:
// Получаем session_token из разных источников (приоритет: Edit Fields11 > Redis Trigger)
const sessionToken = $('Edit Fields11').first().json.session_token
|| $('Redis Trigger').first().json.message.session_id
|| null;
// Если session_token недоступен, генерируем временный ключ
if (!sessionToken) {
console.warn('⚠️ session_token не найден, используем временный ключ');
}
// Используем session_token для Redis ключа (claim_id будет сгенерирован позже)
const redisKey = `ocr_events:${sessionToken || 'temp-' + Date.now()}`;
2. Создать новый узел claimsave_primary (сохранение первичного черновика)
Позиция: После узла Code4, перед push_wizard1
Назначение: Сохранить первичный черновик сразу после генерации wizard_plan
SQL запрос:
-- $1 = payload_json (jsonb) - полный payload с wizard_plan, problem_description, AI Agent1, AI Agent13 и т.д.
-- $2 = session_token (text) - сессия пользователя (используем для связи, claim_id генерируем позже)
-- $3 = unified_id (text, опционально) - unified_id пользователя
WITH partial AS (
SELECT
$1::jsonb AS p,
$2::text AS session_token_str,
NULLIF($3::text, '') AS unified_id_str
),
-- Находим существующую запись по session_token или создаем новую
claim_lookup AS (
SELECT
COALESCE(
(SELECT id FROM clpr_claims WHERE session_token = partial.session_token_str LIMIT 1),
gen_random_uuid()
) AS claim_uuid
FROM partial
),
-- Если записи нет, создаем её
claim_created AS (
INSERT INTO clpr_claims (
id,
session_token,
unified_id,
channel,
type_code,
status_code,
payload,
created_at,
updated_at,
expires_at
)
SELECT
claim_lookup.claim_uuid,
partial.session_token_str,
partial.unified_id_str,
'web_form',
COALESCE(partial.p->>'type_code', 'consumer'),
'draft',
jsonb_build_object(
-- claim_id будет сгенерирован позже, пока NULL
'claim_id', NULL,
'problem_description', partial.p->>'problem_description',
'wizard_plan',
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,
'answers_prefill',
CASE
WHEN partial.p->>'answers_prefill' IS NOT NULL
THEN (partial.p->>'answers_prefill')::jsonb
WHEN partial.p->'answers_prefill' IS NOT NULL AND jsonb_typeof(partial.p->'answers_prefill') = 'array'
THEN partial.p->'answers_prefill'
ELSE '[]'::jsonb
END,
'coverage_report',
CASE
WHEN partial.p->>'coverage_report' IS NOT NULL
THEN (partial.p->>'coverage_report')::jsonb
WHEN partial.p->'coverage_report' IS NOT NULL AND jsonb_typeof(partial.p->'coverage_report') = 'object'
THEN partial.p->'coverage_report'
ELSE NULL
END,
-- Данные из AI Agent1 (факты)
'ai_agent1_facts',
CASE
WHEN partial.p->'ai_agent1_facts' IS NOT NULL AND jsonb_typeof(partial.p->'ai_agent1_facts') = 'object'
THEN partial.p->'ai_agent1_facts'
ELSE NULL
END,
-- Данные из AI Agent13 (RAG ответ)
'ai_agent13_rag',
CASE
WHEN partial.p->>'ai_agent13_rag' IS NOT NULL
THEN (partial.p->>'ai_agent13_rag')::jsonb
WHEN partial.p->'ai_agent13_rag' IS NOT NULL AND jsonb_typeof(partial.p->'ai_agent13_rag') = 'object'
THEN partial.p->'ai_agent13_rag'
ELSE NULL
END,
'phone', partial.p->>'phone',
'email', partial.p->>'email'
),
now(),
now(),
now() + interval '14 days'
FROM partial, claim_lookup
WHERE NOT EXISTS (
SELECT 1 FROM clpr_claims WHERE id = claim_lookup.claim_uuid
)
ON CONFLICT (id) DO NOTHING
RETURNING id
),
-- Получаем финальный UUID
claim_final AS (
SELECT
CASE
WHEN EXISTS (SELECT 1 FROM claim_created)
THEN (SELECT id FROM claim_created LIMIT 1)
ELSE claim_lookup.claim_uuid
END AS claim_uuid
FROM claim_lookup
),
-- Обновляем существующую запись (если есть)
upd AS (
UPDATE clpr_claims c
SET
unified_id = COALESCE(partial.unified_id_str, c.unified_id),
payload = jsonb_set(
jsonb_set(
jsonb_set(
jsonb_set(
COALESCE(c.payload, '{}'::jsonb),
'{wizard_plan}',
COALESCE(
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,
c.payload->'wizard_plan'
),
true
),
'{ai_agent1_facts}',
COALESCE(
CASE
WHEN partial.p->'ai_agent1_facts' IS NOT NULL AND jsonb_typeof(partial.p->'ai_agent1_facts') = 'object'
THEN partial.p->'ai_agent1_facts'
ELSE NULL
END,
c.payload->'ai_agent1_facts'
),
true
),
'{ai_agent13_rag}',
COALESCE(
CASE
WHEN partial.p->>'ai_agent13_rag' IS NOT NULL
THEN (partial.p->>'ai_agent13_rag')::jsonb
WHEN partial.p->'ai_agent13_rag' IS NOT NULL AND jsonb_typeof(partial.p->'ai_agent13_rag') = 'object'
THEN partial.p->'ai_agent13_rag'
ELSE NULL
END,
c.payload->'ai_agent13_rag'
),
true
),
'{problem_description}',
COALESCE(partial.p->>'problem_description', c.payload->>'problem_description'),
true
),
updated_at = now(),
expires_at = now() + interval '14 days'
FROM partial, claim_final
WHERE c.id = claim_final.claim_uuid
AND EXISTS (SELECT 1 FROM claim_lookup WHERE claim_uuid = c.id)
RETURNING c.id, c.payload
)
SELECT
(SELECT jsonb_build_object(
'claim_id', u.id::text,
'session_token', partial.session_token_str,
'status_code', 'draft',
'payload', COALESCE(u.payload, jsonb_build_object())
)
FROM claim_final cf, partial
LEFT JOIN upd u ON true
LIMIT 1) AS claim;
Параметры в n8n:
$1 = {{ 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 (факты)
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_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 = {{ $('Edit Fields11').first().json.session_token || $('Redis Trigger').first().json.message.session_id }}
$3 = {{ $('Edit Fields10').first().json.unified_id || $('Redis Trigger').first().json.message.unified_id || null }}
3. Исправить узел claimsave (для последующих обновлений)
Текущий queryReplacement:
={{ $json.payload_partial_json }}, {{ $('Redis Trigger').item.json.message.claim_id }}
Проблема: Используется claim_id из Redis Trigger, который может быть недоступен. Также SQL ищет запись по claim_id, но на этапе первичного черновика claim_id может быть NULL.
Исправленный queryReplacement:
={{ $json.payload_partial_json }}, {{ $('Edit Fields11').first().json.session_token || $('Redis Trigger').first().json.message.session_id }}
Также нужно обновить SQL в узле claimsave - искать запись по session_token вместо claim_id:
-- Вместо:
WHERE payload->>'claim_id' = partial.claim_id_str
-- Использовать:
WHERE session_token = partial.session_token_str
Примечание: Узел claimsave используется для последующих обновлений (после загрузки файлов, ответов пользователя и т.д.), поэтому он должен работать с уже существующим черновиком, найденным по session_token.
Порядок узлов в workflow
Redis Trigger→ получает событиеget_claime_data1→ получает данные из RedisEdit Fields8→ извлекает поля из сообщенияMerge2→ объединяет данныеGet row(s) in sheet2→ получает шаги формыEdit Fields16→ подготавливает данные для AIAI Agent1→ извлекает факты (полный и короткий)пробрасываем факт фул и факт шорт1→ передает фактыAI Agent13→ генерирует RAG ответoutput_set1→ форматирует выходEdit Fields11→ подготавливает данные для wizardAI Agent12→ генерирует wizard_planCode→ парсит JSONCode4→ форматирует для Redisclaimsave_primary→ СОХРАНЯЕТ ПЕРВИЧНЫЙ ЧЕРНОВИК ⭐push_wizard1→ пушит wizard_plan в Redis для SSE
Что сохраняется в первичный черновик
- ✅
wizard_plan- план вопросов от AI Agent12 - ✅
problem_description- описание проблемы от пользователя - ✅
answers_prefill- предзаполненные ответы (если есть) - ✅
coverage_report- отчёт о покрытии (если есть) - ✅
ai_agent1_facts- данные из AI Agent1 (facts_short, facts_full, problem) - ✅
ai_agent13_rag- RAG ответ от AI Agent13 - ✅
session_token- сессия пользователя (используется для связи, claim_id генерируется позже) - ✅
unified_id- если есть (передается с фронта) - ✅
phone,email- контакты пользователя - ✅
status_code = 'draft'- статус черновика - ⚠️
claim_id- пока NULL, будет сгенерирован позже
Что НЕ сохраняется на этом этапе
- ❌
wizard_answers- ещё нет (пользователь не ответил) - ❌
documents_meta- ещё нет (файлы не загружены)
Данные из AI Agent1 и AI Agent13
Эти данные используются в AI Agent12 для генерации wizard_plan, но также сохраняются в черновик для дальнейшего использования:
-
AI Agent1 →
output(факты полный и короткий):facts_short- краткая суть проблемыfacts_full- полный текст/саммариproblem- классификатор проблемы- Сохраняется в
payload.ai_agent1_facts
-
AI Agent13 →
output(RAG ответ):- Аналитическая справка/правовой ответ из базы знаний
- Сохраняется в
payload.ai_agent13_rag
Они передаются в AI Agent12 через Edit Fields11:
chatInput= описание проблемыoutput= RAG ответ от AI Agent13questions_numbered_html= шаги формы из Google Sheets
Важно: Сохраняем и промежуточные данные (AI Agent1, AI Agent13), и результат (wizard_plan), т.к. они могут пригодиться для дальнейшей обработки.