docs: Обновлён лог сессии - добавлена вторая часть (умная форма Step 2)

This commit is contained in:
AI Assistant
2025-10-28 18:07:38 +03:00
parent 1207222202
commit 6c19392528

View File

@@ -354,3 +354,710 @@ Channels: ocr_events:{claim_id}
**Автор:** AI Assistant (Claude Sonnet 4.5)
**Дата:** 28 октября 2025, 01:00 MSK
---
---
# 📋 Лог сессии: Умная форма Step 2 с AI-обработкой документов
**Дата:** 28 октября 2025 (13:00 - 17:00 MSK)
**Задача:** Рефакторинг Step 2 в интеллектуальную форму с пошаговой загрузкой и AI-обработкой документов
**Статус:** ✅ Успешно завершено
---
## 🎯 Основные задачи
### 1. ✅ Улучшение UX на Step 1 (Policy)
- Добавлены динамические кнопки в модалке OCR:
- **"Продолжить →"** при успешном распознавании → переход на Step 2
- **"Загрузить другой файл"** при ошибке → очистка и повтор
- Добавлен **DEV MODE** панель с кнопкой быстрого перехода на Step 2 без валидации
### 2. ✅ Рефакторинг Step 2 (Details)
**Было:**
- Ручной ввод всех полей (тип события, дата, номер рейса/поезда/парома)
- Загрузка документов как дополнение к ручному вводу
**Стало:**
- **"Интеллектуальная форма"** — AI извлекает данные из документов
- **Пошаговая загрузка** каждого документа с индивидуальной обработкой
- **Модалка обработки** для каждого документа с результатами извлечения
- Ручной ввод только при необходимости (fallback)
### 3. ✅ Определение требований к документам
#### Задержка рейса (`delay_flight`)
1. **Посадочный талон ИЛИ Билет** (обязательно)
- `file_type: flight_delay_boarding_or_ticket`
- `event_type: flight_delay_boarding_or_ticket_processed`
- AI извлекает: номер рейса, дату, маршрут, ФИО, время вылета
2. **Подтверждение задержки** (обязательно, до 3 файлов)
- `file_type: flight_delay_confirmation`
- `event_type: flight_delay_confirmation_processed`
- Справка от АК, email/SMS, ИЛИ фото табло
- AI извлекает: время задержки, причину, фактическое время вылета
#### Отмена рейса (`cancel_flight`)
1. **Билет** (обязательно)
- `file_type: flight_cancel_ticket`
- `event_type: flight_cancel_ticket_processed`
2. **Уведомление об отмене** (обязательно, до 3 файлов)
- `file_type: flight_cancel_notice`
- `event_type: flight_cancel_notice_processed`
- Письмо/SMS от АК, фото табло
#### Пропуск стыковки (`missed_connection`)
1. **Рейс отправления: Посадочный талон ИЛИ Билет** (обязательно)
- `file_type: missed_connection_first_boarding_or_ticket`
- `event_type: missed_connection_first_boarding_or_ticket_processed`
2. **Рейс прибытия: Билет на пропущенный рейс** (обязательно)
- `file_type: missed_connection_second_ticket`
- `event_type: missed_connection_second_ticket_processed`
3. **Подтверждение задержки первого рейса** (опционально, до 3 файлов)
- `file_type: missed_connection_delay_proof`
- `event_type: missed_connection_delay_proof_processed`
#### Задержка/отмена поезда (`delay_train`, `cancel_train`)
1. **Билет** (обязательно)
- `file_type: train_delay_ticket` / `train_cancel_ticket`
2. **Справка о задержке/отмене** (обязательно, до 3 файлов)
- `file_type: train_delay_certificate` / `train_cancel_certificate`
- Справка от ЖД, фото табло
#### Задержка/отмена парома/круиза (`delay_ferry`, `cancel_ferry`)
1. **Билет/Бронь** (обязательно)
- `file_type: ferry_delay_ticket` / `ferry_cancel_ticket`
2. **Подтверждение задержки/отмены** (обязательно, до 3 файлов)
- `file_type: ferry_delay_confirmation` / `ferry_cancel_confirmation`
- Справка, email, фото расписания
### 4. ✅ Уникальные `file_type` для каждого документа
**Принцип:** Каждый тип документа → уникальный `file_type` → уникальный `event_type` в Redis
```typescript
// Пример для отмены рейса
{
file_type: "flight_cancel_ticket" // → S3, n8n, DB
event_type: "flight_cancel_ticket_processed" // → Redis pub/sub
}
{
file_type: "flight_cancel_notice"
event_type: "flight_cancel_notice_processed"
}
```
**Почему это важно:**
- n8n разделяет потоки обработки по `file_type`
- Разные AI промпты для каждого типа документа
- Frontend слушает уникальный `event_type` для каждого документа
### 5. ✅ Добавлены DEV MODE кнопки во все 3 шага
**Step 1 (Policy):**
- "Далее → (Step 2) [пропустить]" — авто-заполнение voucher и claim_id
**Step 2 (Details):**
- "← Назад (Step 1)" — возврат назад
- "Далее → (Step 3) [пропустить]" — авто-заполнение eventType, incidentDate, transportNumber
**Step 3 (Payment):**
- "← Назад (Step 2)" — возврат назад
- "✅ Автоподтверждение телефона [dev]" — автозаполнение всех полей + setIsPhoneVerified(true)
- "🚀 Отправить [пропустить]" — автозаполнение + submit
---
## 🛠️ Технические изменения
### Файл: `frontend/src/components/form/Step1Policy.tsx`
#### Изменение 1: Динамические кнопки в модалке OCR
**Было:**
```typescript
footer={[
<Button key="close" onClick={() => setOcrModalVisible(false)}>
Закрыть
</Button>
]}
```
**Стало:**
```typescript
footer={ocrModalContent === 'loading' ? null :
ocrModalContent?.success ? [
<Button key="next" type="primary" onClick={() => {
setOcrModalVisible(false);
onNext(); // Переход на следующий шаг
}}>
Продолжить
</Button>
] : [
<Button key="retry" type="primary" onClick={() => {
setOcrModalVisible(false);
setFileList([]); // Очищаем список файлов
setPolicyNotFound(true); // Показываем форму загрузки снова
}}>
Загрузить другой файл
</Button>
]
}
```
#### Изменение 2: DEV MODE панель
```typescript
<div style={{
marginTop: 24,
padding: 16,
background: '#f0f0f0',
borderRadius: 8,
border: '2px dashed #999'
}}>
<div style={{ marginBottom: 8, fontSize: 12, color: '#666', fontWeight: 'bold' }}>
🔧 DEV MODE - Быстрая навигация (без валидации)
</div>
<Button
type="dashed"
onClick={() => {
const devData = {
voucher: 'E1000-123456789',
claim_id: `CLM-DEV-${Math.random().toString(36).substr(2, 6).toUpperCase()}`,
};
updateFormData(devData);
onNext();
}}
>
Далее (Step 2) [пропустить]
</Button>
</div>
```
### Файл: `frontend/src/components/form/Step2Details.tsx`
#### Полный рефакторинг!
**Бэкап старой версии:** `Step2Details.OLD_MANUAL_INPUT.tsx`
**Новая структура:**
1. **`DOCUMENT_CONFIGS`** — конфигурация документов для каждого типа события:
```typescript
const DOCUMENT_CONFIGS = {
delay_flight: [
{
name: "Посадочный талон ИЛИ Билет",
field: "boarding_or_ticket",
file_type: "flight_delay_boarding_or_ticket",
required: true,
maxFiles: 1,
description: "Посадочный талон (boarding pass) или билет (ticket/booking)",
aiPromptFocus: "Извлеки: номер рейса, дату, маршрут, ФИО пассажира, время вылета"
},
// ... остальные документы
],
cancel_flight: [...],
// ... остальные типы событий
};
```
2. **Пошаговая загрузка документов:**
```typescript
const [currentDocIndex, setCurrentDocIndex] = useState(0);
const currentDoc = requiredDocs[currentDocIndex];
// После успешной загрузки
if (currentDocIndex < requiredDocs.length - 1) {
setCurrentDocIndex(prev => prev + 1);
} else {
// Все документы загружены
onNext();
}
```
3. **Модалка обработки для каждого документа:**
```typescript
<Modal
title="Обработка документа"
visible={processingModalVisible}
footer={processingModalContent === 'loading' ? null : [
<Button type="primary" onClick={handleContinueAfterProcessing}>
{currentDocIndex < requiredDocs.length - 1
? 'Продолжить к следующему документу →'
: 'Далее (Step 3) →'
}
</Button>
]}
>
{processingModalContent === 'loading' ? (
<div style={{textAlign: 'center', padding: 24}}>
<Spin size="large" />
<p>Обрабатываем документ...</p>
</div>
) : (
<div>
<Alert type="success" message="✅ Документ обработан" />
<pre>{JSON.stringify(processingModalContent, null, 2)}</pre>
</div>
)}
</Modal>
```
4. **SSE для каждого документа с уникальным `event_type`:**
```typescript
const eventSource = new EventSource(
`${API_BASE_URL}/events/${claimId}?event_type=${currentDoc.file_type}_processed`
);
eventSource.onmessage = (event) => {
const result = JSON.parse(event.data);
if (result.event_type === `${currentDoc.file_type}_processed`) {
setProcessingModalContent(result.data);
}
};
```
#### DEV MODE панель:
```typescript
<div style={{ marginTop: 24, padding: 16, background: '#f0f0f0' }}>
<Button onClick={onPrev}> Назад (Step 1)</Button>
<Button
type="dashed"
onClick={() => {
const devData = { eventType: 'delay_flight', incidentDate: dayjs(), transportNumber: 'TEST123' };
updateFormData(devData);
onNext();
}}
>
Далее (Step 3) [пропустить]
</Button>
</div>
```
### Файл: `frontend/src/components/form/Step3Payment.tsx`
#### DEV MODE панель (3 кнопки):
```typescript
<Button onClick={onPrev}> Назад (Step 2)</Button>
<Button
type="dashed"
onClick={() => {
setIsPhoneVerified(true);
const devData = {
fullName: 'Тест Тестов',
email: 'test@test.ru',
phone: '+79991234567',
paymentMethod: 'sbp',
bankName: 'sberbank',
};
updateFormData(devData);
message.success('DEV: Телефон автоматически подтверждён');
}}
>
Автоподтверждение телефона [dev]
</Button>
<Button
type="primary"
onClick={() => {
setIsPhoneVerified(true);
const devData = {...};
updateFormData(devData);
onSubmit();
}}
>
🚀 Отправить [пропустить]
</Button>
```
---
## 🐛 Проблемы и их решения
### Проблема 1: Синтаксические ошибки на фронте
**Симптом:**
```
чета шляпа у нас на фронте
```
**Диагностика:**
- Пользователь сообщил "что то не того"
- Проверка файлов показала **дублирующийся код** после закрывающих тегов компонентов
**Найденные проблемы:**
1. **`Step1Policy.tsx`** (строки 659-820):
- Дублирован весь DEV MODE блок после `</div>` компонента
- Код был просто скопирован повторно
2. **`Step3Payment.tsx`** (после строки 381):
- Дублирован обрезанный фрагмент DEV панели
- Неполный JSX
**Решение:**
```bash
# Удалены дублирующиеся блоки
# Step1Policy.tsx: строки 659-820 удалены
# Step3Payment.tsx: строки после 381 удалены
# Rebuild frontend
docker-compose build frontend
docker-compose up -d frontend
```
**Коммиты:**
- `2999951` - fix: Удалён дублирующийся код в Step1Policy.tsx
- `1207222` - fix: Удалён дублирующийся код в Step3Payment.tsx
### Проблема 2: PostgreSQL INSERT не возвращает данные в n8n
**Симптом:**
```json
{
"s3_url": null,
"file_id": null,
"error": {
"message": "422 - \"{\\\"detail\\\":[{\\\"type\\\":\\\"string_type\\\",\\\"loc\\\":[\\\"body\\\",\\\"file_url\\\"],\\\"msg\\\":\\\"Input should be a valid string\\\",\\\"input\\\":null}]}\""
}
}
```
**Причина:**
1. `INSERT INTO claim_files` не вернул `file_id` и `s3_url`
2. Выяснилось: запись в `claims` с данным `claim_number` не существует
3. Foreign key `claim_id` не может быть установлен → INSERT падает
4. `file_size` передан как `"4.47 MB"` вместо числа в байтах
**Решение:**
Создан UPSERT запрос с CTE (Common Table Expression):
```sql
WITH upserted_claim AS (
INSERT INTO claims (
claim_number, voucher, session_id, status, created_at, updated_at
) VALUES (
$1, $2, $3, 'draft', NOW(), NOW()
)
ON CONFLICT (claim_number)
DO UPDATE SET
updated_at = NOW(),
voucher = COALESCE(EXCLUDED.voucher, claims.voucher)
RETURNING id as claim_id
)
INSERT INTO claim_files (
claim_id, file_type, original_name, s3_key, s3_url,
file_size, mime_type, ocr_status, uploaded_at
)
SELECT
upserted_claim.claim_id,
$4, $5, $6, $7, $8, $9, 'pending', NOW()
FROM upserted_claim
RETURNING id as file_id, s3_url, ocr_status;
```
**Параметры:**
```javascript
[
claim_number, // $1
voucher, // $2
session_id, // $3
file_type, // $4
original_name, // $5
s3_key, // $6
s3_url, // $7
file_size, // $8 (число в байтах!)
mime_type // $9
]
```
**Преимущества:**
- ✅ Атомарная операция
- ✅ Идемпотентность (можно запускать повторно)
- ✅ Всегда создаёт `claims` если его нет
- ✅ Обновляет `updated_at` если уже есть
- ✅ Возвращает `file_id` и `s3_url` для следующих шагов
---
## ✅ Тестирование
### Тест 1: Загрузка билета на отмену рейса
**Файл:** "Билет Романова.pdf"
**Claim ID:** CLM-2025-10-28-33ID32
**file_type:** `flight_cancel_ticket`
**Результат Redis:**
```json
{
"claim_id": "CLM-2025-10-28-33ID32",
"event": {
"event_type": "flight_cancel_ticket_processed",
"status": "completed",
"message": "✅ Документ обработан: flight_cancel_ticket",
"data": {
"output": {
"is_flight_doc": "yes",
"document_type": "e-ticket",
"ticket_number": "2222411714956",
"passengers": [{
"full_name": "ROMANOVA ANASTASIIA",
"doc_number": "774099576"
}],
"itinerary": [{
"flight_number": "A4-6025",
"departure": {
"airport_iata": "MRV",
"date_local": "2025-09-30",
"time_local": "16:25"
},
"arrival": {
"airport_iata": "TLV",
"time_local": "20:00"
}
}]
}
}
}
}
```
**Backend лог:**
```
16:46:29 - 📥 Received message type: message
16:46:29 - 📦 Raw event data: {"claim_id":"CLM-2025-10-28-33ID32",...}
16:46:29 - 📦 Unwrapped n8n Redis format for CLM-2025-10-28-33ID32
16:46:29 - 📤 Sending event to client: completed
16:46:29 - ✅ Task CLM-2025-10-28-33ID32 finished, closing SSE
```
**Результат:** ✅ Полный успех!
- S3 upload ✅
- PostgreSQL UPSERT ✅
- OCR/AI обработка ✅
- Redis publish ✅
- Backend SSE ✅
- Frontend получил данные ✅
---
## 📊 Архитектура Step 2 (новая)
```
┌─────────────────────────────────────────────────────────────┐
│ Step 2: Details (NEW) │
│ │
│ 1. Выбор типа события (eventType) │
│ ↓ │
│ 2. DOCUMENT_CONFIGS определяет список документов │
│ ↓ │
│ 3. Пошаговая загрузка каждого документа: │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Документ 1: Посадочный талон │ │
│ │ - Upload компонент │ │
│ │ - POST /upload → n8n webhook │ │
│ │ - file_type: "flight_delay_boarding_or_ticket" │ │
│ │ - SSE: event_type = "..._processed" │ │
│ │ - Модалка: "Обрабатываем..." │ │
│ │ - Результат: extracted data │ │
│ │ - Кнопка: "Продолжить к следующему" │ │
│ └────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Документ 2: Подтверждение задержки │ │
│ │ - (аналогично) │ │
│ │ - file_type: "flight_delay_confirmation" │ │
│ │ - Может быть до 3 файлов │ │
│ │ - Кнопка: "Далее (Step 3)" │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ 4. После загрузки всех документов → Step 3 │
└─────────────────────────────────────────────────────────────┘
```
### Data Flow для одного документа:
```
Frontend n8n Backend Redis
│ │ │ │
│ POST /upload │ │ │
├────────────────────>│ │ │
│ {claim_id, │ │ │
│ file_type, │ │ │
│ file} │ │ │
│ │ │ │
│ SSE connect │ │ │
├────────────────────────────────────────────>│ │
│ /events/CLM-XXX? │ │ │
│ event_type= │ │ │
│ flight_..._processed│ │ │
│ │ │ │
│ │ 1. Upload to S3 │ │
│ │ 2. UPSERT claims │ │
│ │ 3. INSERT claim_files │ │
│ │ 4. OCR Service │ │
│ │ 5. AI Vision │ │
│ │ 6. PUBLISH │ │
│ ├────────────────────────────────────────────>│
│ │ {event_type: │ │
│ │ "..._processed", │ │
│ │ data: {...}} │ │
│ │ │ │
│ │ │<──────────────────┤
│ │ │ SUBSCRIBE │
│ │ │ ocr_events:CLM-XXX │
│<────────────────────────────────────────────┤ │
│ SSE: data: {event_type, data} │ │
│ │ │ │
│ Show modal: │ │ │
│ "✅ Обработано" │ │ │
│ Display data │ │ │
│ │ │ │
│ User clicks │ │ │
│ "Continue" → │ │ │
│ next document │ │ │
│ (or Step 3) │ │ │
```
---
## 🎯 Логика обработки результатов AI (спроектирована)
### Предложенная структура валидации:
```typescript
const handleOcrResult = (event) => {
const { output } = event.data;
// Проверка 1: Это правильный тип документа?
if (output.is_flight_doc !== "yes") {
return { success: false, message: "❌ Это не авиадокумент" };
}
// Проверка 2: Извлечены ли критичные данные?
const firstFlight = output.itinerary?.[0];
const criticalFields = {
flightNumber: firstFlight?.flight_number,
departureDate: firstFlight?.departure?.date_local,
departureAirport: firstFlight?.departure?.airport_iata,
arrivalAirport: firstFlight?.arrival?.airport_iata,
passengerName: output.passengers?.[0]?.full_name
};
const missing = Object.entries(criticalFields)
.filter(([_, value]) => !value)
.map(([key]) => key);
if (missing.length === 0) {
return { success: true, message: "✅ Билет распознан успешно!" };
} else {
return {
success: "partial",
message: "⚠️ Билет распознан, но не хватает данных",
missingFields: missing
};
}
};
```
### 3 сценария UI:
**SUCCESS:** Все данные извлечены
- ✅ Показать извлечённые данные
- Кнопка: "Продолжить к следующему документу →"
**PARTIAL:** Документ валидный, но данные неполные
- ⚠️ Показать что извлечено + что отсутствует
- 3 кнопки:
1. "📸 Загрузить документ лучшего качества"
2. "✍️ Ввести недостающие данные вручную"
3. "Продолжить с доступными данными"
**FAIL:** Неправильный тип документа
- ❌ Ошибка
- 2 кнопки:
1. "Загрузить другой файл"
2. "Ввести данные вручную"
---
## 📝 Git Commits
```bash
# Commit history (от старого к новому)
6fe1459 - backup: Сохранён старый Step2Details с ручным вводом полей
122af07 - feat: Умная форма Step2 с автоматическим распознаванием документов
9084d75 - feat: Пошаговая загрузка документов с модалкой на Step 2
2999951 - fix: Удалён дублирующийся код в Step1Policy.tsx
1207222 - fix: Удалён дублирующийся код в Step3Payment.tsx
```
**Push:**`origin/main` (все коммиты)
---
## 📈 Метрики
**Время выполнения сессии:** ~4 часа
**Количество коммитов:** 5
**Изменённых файлов:** 4 (Step1Policy, Step2Details, Step2Details.OLD, Step3Payment)
**Строк добавлено:** ~800
**Строк удалено:** ~200 (дубликаты) + ~400 (рефакторинг Step2)
**Frontend rebuilds:** 3
**Тестовых загрузок:** 3
**Redis событий обработано:** 3
---
## 🔗 Ссылки
- Frontend: http://147.45.146.17:5173
- Backend API: http://localhost:8100
- Gitea: http://147.45.146.17:3002/negodiy/erv-platform
- n8n: http://147.45.146.17:5678
- N8N SQL Queries: `/erv_platform/N8N_SQL_QUERIES.md`
---
## 📝 Важные заметки
### Redis Password (обновлено)
```
Host: crm.clientright.ru
Port: 6379
Password: CRM_Redis_Pass_2025_Secure!
(из /etc/redis/redis.conf)
```
### PostgreSQL UPSERT для n8n
Сохранён в `N8N_SQL_QUERIES.md` для использования в webhook nodes.
### Структура `file_type` → `event_type`
```
file_type: "flight_cancel_ticket"
event_type: "flight_cancel_ticket_processed"
Формула: event_type = file_type + "_processed"
```
### DEV MODE
Все три шага имеют панель для быстрой навигации без заполнения форм — ускоряет разработку и тестирование.
---
**Статус:** ✅ Успешно завершено
**Автор:** AI Assistant (Claude Sonnet 4.5)
**Дата:** 28 октября 2025, 17:00 MSK