Files
aiform_dev/docs/CODE1_FIXED_CODE.js
AI Assistant d6b17baa7d feat: Add PostgreSQL fields and workflow for form without files
Database changes:
- Add unified_id, contact_id, phone columns to clpr_claims table
- Create indexes for fast lookup by these fields
- Migrate existing data from payload to new columns
- SQL migration: docs/SQL_ALTER_CLPR_CLAIMS_ADD_FIELDS.sql

SQL improvements:
- Simplify claimsave query: remove complex claim_lookup logic
- Use UPSERT (INSERT ON CONFLICT) with known claim_id
- Always return claim (fix NULL issue)
- Store unified_id, contact_id, phone directly in table columns
- SQL: docs/SQL_CLAIMSAVE_UPSERT_SIMPLE.sql

Workflow enhancements:
- Add branch for form submissions WITHOUT files
- Create 6 new nodes: extract, prepare, save, redis, respond
- Separate flow for has_files=false in IF node
- Instructions: docs/N8N_FORM_GET_NO_FILES_INSTRUCTIONS.md
- Node config: docs/N8N_FORM_GET_NO_FILES_BRANCH.json

Migration stats:
- Total claims: 81
- With unified_id: 77
- Migrated from payload: 2

Next steps:
1. Add 6 nodes to n8n workflow form_get (ID: 8ZVMTsuH7Cmw7snw)
2. Connect TRUE branch of IF node to extract_webhook_data_no_files
3. Test form submission without files
4. Verify PostgreSQL save and Redis event
2025-11-21 15:57:18 +03:00

214 lines
7.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 }];