Files
aiform_dev/docs/CODE1_FIXED_CODE.js

215 lines
7.9 KiB
JavaScript
Raw Normal View History

// Code node (JavaScript). Input: items[0].json = либо объект, либо массив таких объектов, как ты прислал.
// Output: по одному нормализованному объекту на кейс.
// Никаких внешних зависимостей, всё на ванильном JS.
function toNullish(v) {
if (v === undefined || v === null) return null;
if (typeof v === 'string' && v.trim() === '') return null;
return v;
}
function pick(o, path, def = null) {
try {
return toNullish(path.split('.').reduce((acc, k) => (acc == null ? undefined : acc[k]), o));
} catch {
return def;
}
}
function mapDocuments(docs = []) {
// Проверяем, что docs не null и является массивом
if (!docs || !Array.isArray(docs)) return [];
return docs.map(d => ({
id: toNullish(d.id),
claim_document_id: toNullish(d.id), // у тебя id = claim_document_id
file_id: toNullish(d.file_id),
file_url: toNullish(d.file_url),
file_name: toNullish(d.file_name),
original_file_name: toNullish(d.original_file_name),
field_name: toNullish(d.field_name),
upload_description: toNullish(d.upload_description),
uploaded_at: toNullish(d.uploaded_at),
filename_for_upload: toNullish(d.filename_for_upload),
}));
}
function mapVisionDocs(vds = []) {
// Проверяем, что vds не null и является массивом
if (!vds || !Array.isArray(vds)) return [];
return vds.map(v => ({
claim_document_id: toNullish(v.claim_document_id),
vision_document_id: toNullish(v.vision_document_id),
pages: toNullish(v.pages),
content_sha256: toNullish(v.content_sha256),
vision_text: toNullish(v.vision_text),
vision_pages: Array.isArray(v.vision_pages)
? v.vision_pages.map(p => ({
page: toNullish(p.page),
uid: toNullish(p.uid),
}))
: null,
}));
}
function mapCombinedDocs(cds = []) {
// Проверяем, что cds не null и является массивом
if (!cds || !Array.isArray(cds)) return [];
return cds.map(c => ({
claim_document_id: toNullish(c.claim_document_id),
combined_document_id: toNullish(c.combined_document_id),
pages: toNullish(c.pages),
content_sha256: toNullish(c.content_sha256),
combined_text: toNullish(c.combined_text),
page_summaries: Array.isArray(c.page_summaries)
? c.page_summaries.map(ps => ({
page: toNullish(ps.page),
chars: toNullish(ps.chars),
uid: toNullish(ps.uid),
image_url: toNullish(ps.image_url),
}))
: null,
}));
}
function mapDialogHistory(h = []) {
// ИСПРАВЛЕНО: Проверяем, что h не null и является массивом
if (!h || !Array.isArray(h)) return [];
return h.map(m => ({
id: toNullish(m.id),
role: toNullish(m.role),
message: toNullish(m.message),
message_type: toNullish(m.message_type),
tg_message_id: toNullish(m.tg_message_id),
created_at: toNullish(m.created_at),
}));
}
function mapCoverageReport(cr = null) {
if (!cr) return null;
return {
questions: Array.isArray(cr.questions)
? cr.questions.map(q => ({
name: toNullish(q.name),
value: toNullish(q.value),
status: toNullish(q.status),
source: toNullish(q.source),
confidence: toNullish(q.confidence),
}))
: null,
docs_missing: Array.isArray(cr.docs_missing) ? cr.docs_missing : null,
docs_received: Array.isArray(cr.docs_received) ? cr.docs_received : null,
};
}
function normalizeOne(src) {
const claim = src.claim ?? {};
const userInfo = src.user_info ?? {};
const propertyName = claim.propertyName ?? {};
// answers_parsed уже есть в claim; не мудрим — возвращаем как есть, пустоты -> null
const answersParsed = claim.answers_parsed
? Object.fromEntries(
Object.entries(claim.answers_parsed).map(([k, v]) => [k, toNullish(v)])
)
: null;
// wizard план (часто нужен на фронте) — оставим ключевые поля
let wizard = null;
try {
const parsed = typeof claim.wizard_plan === 'string'
? JSON.parse(claim.wizard_plan)
: (claim.wizard_plan_parsed ?? null);
if (parsed) {
wizard = {
version: toNullish(parsed.version),
case_type: toNullish(parsed.case_type),
goals: Array.isArray(parsed.goals) ? parsed.goals : null,
documents: Array.isArray(parsed.documents) ? parsed.documents : null,
questions: Array.isArray(parsed.questions) ? parsed.questions : null,
risks: Array.isArray(parsed.risks) ? parsed.risks : null,
deadlines: Array.isArray(parsed.deadlines) ? parsed.deadlines : null,
ask_order: Array.isArray(parsed.ask_order) ? parsed.ask_order : null,
notes: toNullish(parsed.notes),
user_text: toNullish(parsed.user_text),
};
}
} catch {
wizard = null;
}
// Склеиваем user — берём user_info, плюс propertyName на всякий, и то, что лежит в диалогах
const user = {
channel: toNullish(userInfo.channel ?? propertyName.channel),
user_id: toNullish(userInfo.user_id ?? propertyName.user_id),
unified_id: toNullish(userInfo.unified_id ?? propertyName.unified_id),
telegram_id: toNullish(userInfo.telegram_id ?? propertyName.telegram_id ?? claim.telegram_id),
session_token: toNullish(userInfo.session_token ?? propertyName.session_token ?? claim.session_token),
};
// Собираем
const out = {
case: {
id: toNullish(pick(claim, 'id')),
prefix: toNullish(pick(claim, 'prefix')),
channel: toNullish(pick(claim, 'channel')),
type_code: toNullish(pick(claim, 'type_code')),
status_code: toNullish(pick(claim, 'status_code')),
created_at: toNullish(pick(claim, 'created_at')),
updated_at: toNullish(pick(claim, 'updated_at')),
telegram_id: toNullish(pick(claim, 'telegram_id')),
session_token: toNullish(pick(claim, 'session_token')),
unified_id: toNullish(pick(claim, 'unified_id')),
case_type: toNullish(pick(claim, 'case_type')),
},
user, // см. выше
answers: answersParsed,
// что загрузили
documents: mapDocuments(src.documents),
// OCR/Vision/Combined, если есть
vision_docs: mapVisionDocs(src.vision_docs),
combined_docs: mapCombinedDocs(src.combined_docs),
// что там в "coverage_report" (кто что заполнил/не заполнил в мастере)
coverage_report: mapCoverageReport(pick(claim, 'coverage_report')),
// история чата (ID, роли, тексты)
dialog_history: mapDialogHistory(src.dialog_history),
// на всякий — куда и что складывали на S3 в момент сохранения
s3_manifest: {
session_token: toNullish(pick(claim, 'session_token')),
documents_meta: Array.isArray(claim.documents_meta) ? claim.documents_meta : null,
},
// флаги/риски, что засетили при сохранении
risks: Array.isArray(claim.risks) ? claim.risks : null,
// план (wizard), как есть — пригодится фронту и валидаторам
wizard_plan: wizard,
};
return out;
}
// === entrypoint ===
const raw = items[0]?.json ?? {};
const arr = Array.isArray(raw) ? raw : [raw];
// опциональный фильтр по claim_id, если в item передадут { claim_id: "..." }
const claimIdFilter = items[0]?.json?.claim_id || items[0]?.json?.claimId || null;
// Прогоняем всё, отдаём по одному Item на кейс
const results = arr
.map(normalizeOne)
.filter(obj => (claimIdFilter ? obj.case.id === claimIdFilter : true))
.map(obj => ({ json: obj }));
return results.length ? results : [{ json: null }];
feat: Add claim plan confirmation flow via Redis SSE Problem: - After wizard form submission, need to wait for claim data from n8n - Claim data comes via Redis channel claim:plan:{session_token} - Need to display confirmation form with claim data Solution: 1. Backend: Added SSE endpoint /api/v1/claim-plan/{session_token} - Subscribes to Redis channel claim:plan:{session_token} - Streams claim data from n8n to frontend - Handles timeouts and errors gracefully 2. Frontend: Added subscription to claim:plan channel - StepWizardPlan: After form submission, subscribes to SSE - Waits for claim_plan_ready event - Shows loading message while waiting - On success: saves claimPlanData and shows confirmation step 3. New component: StepClaimConfirmation - Displays claim confirmation form in iframe - Receives claimPlanData from parent - Generates HTML form (placeholder - should call n8n for real HTML) - Handles confirmation/cancellation via postMessage 4. ClaimForm: Added conditional step for confirmation - Shows StepClaimConfirmation when showClaimConfirmation=true - Step appears after StepWizardPlan - Only visible when claimPlanData is available Flow: 1. User fills wizard form → submits 2. Form data sent to n8n via /api/v1/claims/wizard 3. Frontend subscribes to SSE /api/v1/claim-plan/{session_token} 4. n8n processes data → publishes to Redis claim:plan:{session_token} 5. Backend receives → streams to frontend via SSE 6. Frontend receives → shows StepClaimConfirmation 7. User confirms → proceeds to next step Files: - backend/app/api/events.py: Added stream_claim_plan endpoint - frontend/src/components/form/StepWizardPlan.tsx: Added subscribeToClaimPlan - frontend/src/components/form/StepClaimConfirmation.tsx: New component - frontend/src/pages/ClaimForm.tsx: Added confirmation step to steps array
2025-11-24 13:36:14 +03:00