fix: убран claim_id, используется только session_id на ранних этапах
- Убрана проверка claim_id из StepDescription.tsx - Заменен claim_id на session_id в StepWizardPlan.tsx для SSE подключения - Убран claim_id из запросов к API и сохранения в Step1Phone - Обновлен backend для работы с опциональным claim_id - Добавлена документация по исправлению узла claimsave для первичного черновика - Добавлены SQL запросы и примеры кода для n8n workflow
This commit is contained in:
@@ -471,7 +471,7 @@ async def publish_ticket_form_description(payload: TicketFormDescriptionRequest)
|
||||
event = {
|
||||
"type": "ticket_form_description",
|
||||
"session_id": payload.session_id,
|
||||
"claim_id": payload.claim_id,
|
||||
"claim_id": payload.claim_id, # Опционально - может быть None
|
||||
"phone": payload.phone,
|
||||
"email": payload.email,
|
||||
"description": payload.problem_description.strip(),
|
||||
@@ -480,7 +480,7 @@ async def publish_ticket_form_description(payload: TicketFormDescriptionRequest)
|
||||
}
|
||||
logger.info(
|
||||
"📝 TicketForm description received",
|
||||
extra={"session_id": payload.session_id, "claim_id": payload.claim_id},
|
||||
extra={"session_id": payload.session_id, "claim_id": payload.claim_id or "not_set"},
|
||||
)
|
||||
await redis_service.publish(channel, json.dumps(event, ensure_ascii=False))
|
||||
logger.info(
|
||||
|
||||
335
ticket_form/docs/CLAIMSAVE_PRIMARY_DRAFT_FIX.md
Normal file
335
ticket_form/docs/CLAIMSAVE_PRIMARY_DRAFT_FIX.md
Normal file
@@ -0,0 +1,335 @@
|
||||
# Исправление узла `claimsave` для сохранения первичного черновика
|
||||
|
||||
## Проблемы
|
||||
|
||||
1. **`claim_id` генерируется в другом workflow** - нужно использовать `session_id` для связи, `claim_id` генерировать позже
|
||||
2. **Неправильный `sessionToken` в Code4** - используется `claim_id` вместо `session_token`
|
||||
3. **Нет сохранения первичного черновика** - нужно сохранить сразу после генерации `wizard_plan`
|
||||
4. **Данные из AI Agent1 и AI Agent13 не сохраняются** - они пригодятся, нужно их сохранить в черновик
|
||||
|
||||
## Решение
|
||||
|
||||
### 1. Исправить узел `Code4` (подготовка данных для Redis)
|
||||
|
||||
**Текущий код (строка 459):**
|
||||
```javascript
|
||||
const sessionToken = $('Redis Trigger').first().json.message.claim_id
|
||||
```
|
||||
|
||||
**Проблема:** Используется `claim_id` вместо `session_token` для Redis ключа. `claim_id` может быть недоступен или генерируется позже.
|
||||
|
||||
**Исправленный код:**
|
||||
```javascript
|
||||
// Получаем 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 запрос:**
|
||||
```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`:
|
||||
```sql
|
||||
-- Вместо:
|
||||
WHERE payload->>'claim_id' = partial.claim_id_str
|
||||
|
||||
-- Использовать:
|
||||
WHERE session_token = partial.session_token_str
|
||||
```
|
||||
|
||||
**Примечание:** Узел `claimsave` используется для последующих обновлений (после загрузки файлов, ответов пользователя и т.д.), поэтому он должен работать с уже существующим черновиком, найденным по `session_token`.
|
||||
|
||||
## Порядок узлов в workflow
|
||||
|
||||
1. `Redis Trigger` → получает событие
|
||||
2. `get_claime_data1` → получает данные из Redis
|
||||
3. `Edit Fields8` → извлекает поля из сообщения
|
||||
4. `Merge2` → объединяет данные
|
||||
5. `Get row(s) in sheet2` → получает шаги формы
|
||||
6. `Edit Fields16` → подготавливает данные для AI
|
||||
7. `AI Agent1` → извлекает факты (полный и короткий)
|
||||
8. `пробрасываем факт фул и факт шорт1` → передает факты
|
||||
9. `AI Agent13` → генерирует RAG ответ
|
||||
10. `output_set1` → форматирует выход
|
||||
11. `Edit Fields11` → подготавливает данные для wizard
|
||||
12. `AI Agent12` → генерирует wizard_plan
|
||||
13. `Code` → парсит JSON
|
||||
14. `Code4` → форматирует для Redis
|
||||
15. **`claimsave_primary`** → **СОХРАНЯЕТ ПЕРВИЧНЫЙ ЧЕРНОВИК** ⭐
|
||||
16. `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 Agent13
|
||||
- `questions_numbered_html` = шаги формы из Google Sheets
|
||||
|
||||
**Важно:** Сохраняем и промежуточные данные (AI Agent1, AI Agent13), и результат (`wizard_plan`), т.к. они могут пригодиться для дальнейшей обработки.
|
||||
|
||||
77
ticket_form/docs/CODE4_FIXED.js
Normal file
77
ticket_form/docs/CODE4_FIXED.js
Normal file
@@ -0,0 +1,77 @@
|
||||
// n8n Code node (Run Once) — prepare object for Redis
|
||||
const items = $input.all();
|
||||
|
||||
// 1) Найти первый подходящий элемент с parsed.obj
|
||||
let main = null;
|
||||
for (const it of items) {
|
||||
const j = it.json;
|
||||
if (!j) continue;
|
||||
// возможные места
|
||||
if (j.parsed && j.parsed.obj) { main = j.parsed.obj; break; }
|
||||
if (j.parsed && j.parsed.ok && j.parsed.obj) { main = j.parsed.obj; break; }
|
||||
if (j.output) {
|
||||
// если output — строка JSON
|
||||
try {
|
||||
const parsed = JSON.parse(j.output);
|
||||
if (parsed && parsed.wizard_plan) { main = parsed; break; }
|
||||
} catch (e) {}
|
||||
}
|
||||
if (j.json && j.json.wizard_plan) { main = j.json; break; }
|
||||
}
|
||||
if (!main) {
|
||||
// последний шанс: взять items[0].json
|
||||
main = items[0] ? (items[0].json || items[0]) : null;
|
||||
}
|
||||
if (!main) {
|
||||
throw new Error('Не удалось найти parsed.obj в входных данных');
|
||||
}
|
||||
|
||||
// 2) Гарантии структуры
|
||||
main.wizard_plan = main.wizard_plan || {};
|
||||
main.coverage_report = main.coverage_report || {};
|
||||
main.coverage_report.docs_received = main.coverage_report.docs_received || [];
|
||||
main.wizard_plan.risks = main.wizard_plan.risks || ['DOCS_STATUS_UNKNOWN','EXPECTATION_UNSET'];
|
||||
main.wizard_plan.deadlines = main.wizard_plan.deadlines || [
|
||||
{ type: 'USER_UPLOAD_TTL', duration_hours: 48 },
|
||||
{ type: 'USER_APPROVAL_TTL', duration_hours: 24 }
|
||||
];
|
||||
|
||||
// 3) Добавить примерный документ (state/cities) — если ещё нет такого id
|
||||
const exampleId = 'example_state_cities_json';
|
||||
const already = main.coverage_report.docs_received.find(d => d.id === exampleId);
|
||||
if (!already) {
|
||||
const exampleDoc = {
|
||||
id: exampleId,
|
||||
name: 'state_cities_example.json',
|
||||
type: 'application/json',
|
||||
uploaded_at: new Date().toISOString(),
|
||||
content: {
|
||||
state: 'California',
|
||||
cities: ['Los Angeles', 'San Francisco', 'San Diego']
|
||||
}
|
||||
};
|
||||
main.coverage_report.docs_received.push(exampleDoc);
|
||||
}
|
||||
|
||||
// 4) session token / key
|
||||
// Получаем 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()}`;
|
||||
|
||||
// 5) Возвращаем объект для следующего Redis node
|
||||
return [{
|
||||
json: {
|
||||
redis_key: redisKey,
|
||||
redis_value: main
|
||||
}
|
||||
}];
|
||||
|
||||
41
ticket_form/docs/CODE_CREATE_WEB_CONTACT_FINAL.js
Normal file
41
ticket_form/docs/CODE_CREATE_WEB_CONTACT_FINAL.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// Парсим результат CreateWebContact
|
||||
const rawResult = $node["CreateWebContact"].json.result;
|
||||
|
||||
const contactData = JSON.parse(rawResult); // {"contact_id": "396625", "is_new": false}
|
||||
|
||||
const phone = $('Edit Fields').first().json.phone;
|
||||
|
||||
// Получаем session_id
|
||||
const session_id = $('Edit Fields').first().json.session_id;
|
||||
|
||||
// Получаем unified_id из ноды user_get
|
||||
const unified_id = $('user_get').first().json.unified_id || null;
|
||||
|
||||
// Формируем session для Redis (БЕЗ claim_id, с unified_id)
|
||||
const sessionData = {
|
||||
// claim_id убран - используем только session_id на этих этапах
|
||||
unified_id: unified_id, // ← unified_id из PostgreSQL (получаем от user_get)
|
||||
contact_id: contactData.contact_id, // ← распарсенный ID из CreateWebContact
|
||||
phone: phone,
|
||||
is_new_contact: contactData.is_new, // ← флаг нового контакта
|
||||
status: "draft",
|
||||
current_step: 1,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
documents: {},
|
||||
email: null,
|
||||
bank_name: null
|
||||
};
|
||||
|
||||
return {
|
||||
session: session_id,
|
||||
session_id: session_id, // Добавляем для совместимости
|
||||
unified_id: unified_id, // ✅ Добавляем unified_id в return
|
||||
contact_id: contactData.contact_id,
|
||||
is_new_contact: contactData.is_new,
|
||||
phone: phone,
|
||||
redis_key: `session:${session_id}`, // ✅ Используем session_id для ключа Redis
|
||||
redis_value: JSON.stringify(sessionData),
|
||||
ttl: 604800
|
||||
};
|
||||
|
||||
44
ticket_form/docs/CODE_CREATE_WEB_CONTACT_FIXED.js
Normal file
44
ticket_form/docs/CODE_CREATE_WEB_CONTACT_FIXED.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// Парсим результат CreateWebContact
|
||||
const rawResult = $node["CreateWebContact"].json.result;
|
||||
|
||||
const contactData = JSON.parse(rawResult); // {"contact_id": "396625", "is_new": false}
|
||||
|
||||
const phone = $('Edit Fields').first().json.phone;
|
||||
|
||||
// Получаем session_id
|
||||
const session_id = $('Edit Fields').first().json.session_id;
|
||||
|
||||
// Генерируем claim_id
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
const randomId = Math.random().toString(36).substr(2, 6).toUpperCase();
|
||||
const claim_id = `CLM-${date}-${randomId}`;
|
||||
|
||||
// Формируем session для Redis
|
||||
const sessionData = {
|
||||
claim_id: claim_id,
|
||||
contact_id: contactData.contact_id, // ← распарсенный ID
|
||||
phone: phone,
|
||||
is_new_contact: contactData.is_new, // ← флаг нового контакта
|
||||
status: "draft",
|
||||
current_step: 1,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
voucher: null,
|
||||
event_type: null,
|
||||
documents: {},
|
||||
email: null,
|
||||
bank_name: null
|
||||
};
|
||||
|
||||
return {
|
||||
session: session_id,
|
||||
session_id: session_id, // Добавляем для совместимости
|
||||
claim_id: claim_id,
|
||||
contact_id: contactData.contact_id,
|
||||
is_new_contact: contactData.is_new,
|
||||
phone: phone,
|
||||
redis_key: `session:${session_id}`, // ✅ Исправлено: используем session_id вместо session
|
||||
redis_value: JSON.stringify(sessionData),
|
||||
ttl: 604800
|
||||
};
|
||||
|
||||
@@ -135,7 +135,7 @@ export default function Step1Phone({
|
||||
smsCode: code,
|
||||
contact_id: result.contact_id,
|
||||
unified_id: result.unified_id, // ✅ Unified ID из PostgreSQL (получаем от n8n)
|
||||
claim_id: result.claim_id,
|
||||
// claim_id убран - используем только session_id на этих этапах
|
||||
is_new_contact: result.is_new_contact
|
||||
};
|
||||
|
||||
|
||||
@@ -53,20 +53,14 @@ export default function StepDescription({
|
||||
message.error('Не найден session_id. Попробуйте обновить страницу.');
|
||||
return;
|
||||
}
|
||||
if (!formData.claim_id) {
|
||||
message.error('Не удалось определить номер обращения. Вернитесь на шаг с телефоном.');
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
|
||||
if (useMockWizard && wizardPlanSample?.wizard_plan) {
|
||||
const mockPrefill = buildPrefillMap(wizardPlanSample.answers_prefill);
|
||||
const mockClaimId = wizardPlanSample.claim_id || formData.claim_id;
|
||||
|
||||
updateFormData({
|
||||
problemDescription: safeDescription,
|
||||
claim_id: mockClaimId,
|
||||
wizardPlan: wizardPlanSample.wizard_plan,
|
||||
wizardPlanStatus: 'ready',
|
||||
wizardPrefill: mockPrefill,
|
||||
@@ -85,7 +79,6 @@ export default function StepDescription({
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
session_id: formData.session_id,
|
||||
claim_id: formData.claim_id,
|
||||
phone: formData.phone,
|
||||
email: formData.email,
|
||||
problem_description: safeDescription,
|
||||
|
||||
@@ -339,19 +339,19 @@ export default function StepWizardPlan({
|
||||
}, [formValues, plan, questions, documentGroups, questionFileBlocks, handleDocumentBlocksChange, skippedDocuments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isWaiting || !formData.claim_id || plan) {
|
||||
if (!isWaiting || !formData.session_id || plan) {
|
||||
return;
|
||||
}
|
||||
|
||||
const claimId = formData.claim_id;
|
||||
const source = new EventSource(`/events/${claimId}`);
|
||||
const sessionId = formData.session_id;
|
||||
const source = new EventSource(`/events/${sessionId}`);
|
||||
eventSourceRef.current = source;
|
||||
debugLoggerRef.current?.('wizard', 'info', '🔌 Подключаемся к SSE для плана вопросов', { claim_id: claimId });
|
||||
debugLoggerRef.current?.('wizard', 'info', '🔌 Подключаемся к SSE для плана вопросов', { session_id: sessionId });
|
||||
|
||||
// Таймаут: если план не пришёл за 2 минуты (RAG может работать долго), показываем ошибку
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
setConnectionError('План вопросов не получен. Проверьте, что n8n обработал описание проблемы.');
|
||||
debugLoggerRef.current?.('wizard', 'error', '⏱️ Таймаут ожидания плана вопросов (2 минуты)', { claim_id: claimId });
|
||||
debugLoggerRef.current?.('wizard', 'error', '⏱️ Таймаут ожидания плана вопросов (2 минуты)', { session_id: sessionId });
|
||||
if (eventSourceRef.current) {
|
||||
eventSourceRef.current.close();
|
||||
eventSourceRef.current = null;
|
||||
@@ -360,7 +360,7 @@ export default function StepWizardPlan({
|
||||
|
||||
source.onopen = () => {
|
||||
setConnectionError(null);
|
||||
debugLoggerRef.current?.('wizard', 'info', '✅ SSE соединение открыто', { claim_id: claimId });
|
||||
debugLoggerRef.current?.('wizard', 'info', '✅ SSE соединение открыто', { session_id: sessionId });
|
||||
};
|
||||
|
||||
source.onerror = (error) => {
|
||||
@@ -368,7 +368,7 @@ export default function StepWizardPlan({
|
||||
setConnectionError('Не удалось получить ответ от AI. Попробуйте ещё раз.');
|
||||
source.close();
|
||||
eventSourceRef.current = null;
|
||||
debugLoggerRef.current?.('wizard', 'error', '❌ SSE ошибка (wizard)', { claim_id: claimId });
|
||||
debugLoggerRef.current?.('wizard', 'error', '❌ SSE ошибка (wizard)', { session_id: sessionId });
|
||||
};
|
||||
|
||||
const extractWizardPayload = (incoming: any): any => {
|
||||
@@ -403,7 +403,7 @@ export default function StepWizardPlan({
|
||||
|
||||
// Логируем все события для отладки
|
||||
debugLoggerRef.current?.('wizard', 'info', '📨 Получено SSE событие', {
|
||||
claim_id: claimId,
|
||||
session_id: sessionId,
|
||||
event_type: eventType,
|
||||
has_wizard_plan: Boolean(extractWizardPayload(payload)),
|
||||
payload_keys: Object.keys(payload),
|
||||
@@ -419,7 +419,7 @@ export default function StepWizardPlan({
|
||||
const coverageReport = wizardPayload?.coverage_report;
|
||||
|
||||
debugLoggerRef.current?.('wizard', 'success', '✨ Получен план вопросов', {
|
||||
claim_id: claimId,
|
||||
session_id: sessionId,
|
||||
questions: wizardPlan?.questions?.length || 0,
|
||||
});
|
||||
|
||||
@@ -459,11 +459,11 @@ export default function StepWizardPlan({
|
||||
eventSourceRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [isWaiting, formData.claim_id, plan, updateFormData]);
|
||||
}, [isWaiting, formData.session_id, plan, updateFormData]);
|
||||
|
||||
const handleRefreshPlan = () => {
|
||||
if (!formData.claim_id) {
|
||||
message.error('Не найден claim_id для подписки на события.');
|
||||
if (!formData.session_id) {
|
||||
message.error('Не найден session_id для подписки на события.');
|
||||
return;
|
||||
}
|
||||
setIsWaiting(true);
|
||||
@@ -561,7 +561,7 @@ export default function StepWizardPlan({
|
||||
try {
|
||||
setSubmitting(true);
|
||||
addDebugEvent?.('wizard', 'info', '📤 Отправляем данные визарда в n8n', {
|
||||
claim_id: formData.claim_id,
|
||||
session_id: formData.session_id,
|
||||
});
|
||||
|
||||
const formPayload = new FormData();
|
||||
@@ -570,7 +570,7 @@ export default function StepWizardPlan({
|
||||
if (formData.session_id) formPayload.append('session_id', formData.session_id);
|
||||
if (formData.clientIp) formPayload.append('client_ip', formData.clientIp);
|
||||
if (formData.smsCode) formPayload.append('sms_code', formData.smsCode);
|
||||
if (formData.claim_id) formPayload.append('claim_id', formData.claim_id);
|
||||
// claim_id убран - используем только session_id на этих этапах
|
||||
if (formData.contact_id) formPayload.append('contact_id', String(formData.contact_id));
|
||||
if (formData.project_id) formPayload.append('project_id', String(formData.project_id));
|
||||
if (typeof formData.is_new_contact !== 'undefined') {
|
||||
@@ -1096,12 +1096,12 @@ export default function StepWizardPlan({
|
||||
</>
|
||||
);
|
||||
|
||||
if (!formData.claim_id) {
|
||||
if (!formData.session_id) {
|
||||
return (
|
||||
<Result
|
||||
status="warning"
|
||||
title="Нет claim_id"
|
||||
subTitle="Не удалось определить идентификатор заявки. Вернитесь на предыдущий шаг и попробуйте снова."
|
||||
title="Нет session_id"
|
||||
subTitle="Не удалось определить идентификатор сессии. Вернитесь на предыдущий шаг и попробуйте снова."
|
||||
extra={<Button onClick={onPrev}>Вернуться</Button>}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user