1177 lines
46 KiB
JSON
1177 lines
46 KiB
JSON
{
|
|
"name": "LCBP3 Migration Workflow v2.0.0",
|
|
"nodes": [
|
|
{
|
|
"parameters": {
|
|
"formTitle": "LCBP3 Migration - ตั้งค่าก่อนรัน",
|
|
"formDescription": "กรุณาตั้งค่า Batch และ Excel file ก่อนรัน Migration",
|
|
"formFields": {
|
|
"values": [
|
|
{
|
|
"fieldLabel": "Batch Size",
|
|
"fieldType": "number",
|
|
"placeholder": "10"
|
|
},
|
|
{
|
|
"fieldLabel": "Excel File Path",
|
|
"placeholder": "/home/node/.n8n-files/staging_ai/C22024.xlsx"
|
|
}
|
|
]
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "a0346819-4e97-4208-99c8-e4f958d652fe",
|
|
"name": "Form Trigger",
|
|
"type": "n8n-nodes-base.formTrigger",
|
|
"typeVersion": 2.2,
|
|
"position": [
|
|
-9280,
|
|
4400
|
|
],
|
|
"webhookId": "dd44ab55-df6b-4f3b-a740-6a993ca7ded0",
|
|
"notes": "เปิด URL เพื่อตั้งค่าก่อนรัน"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// ============================================\n// CONFIGURATION v2.0 — ADR-023A Compliant\n// n8n เรียกแค่ DMS Backend API เท่านั้น — ห้ามเรียก Ollama โดยตรง\n// ============================================\nconst formData = $('Form Trigger').first()?.json || {};\nconst batchSizeInput = parseInt(String(formData['Batch Size'] || '0'));\nconst excelFileInput = String(formData['Excel File Path'] || '').trim();\nconst jwtTokenInput = 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1pZ3JhdGlvbl9ib3QiLCJzdWIiOjUsInNjb3BlIjoiR2xvYmFsIiwiaWF0IjoxNzc5NTQwNDk4LCJleHAiOjQ5MzUzMDA0OTh9.TZVzG8u4Cyz8hi0cU3eGaeXinKWKHN3HtYnDcS5p_5I';\nconst resolvedExcelFile = excelFileInput || '/home/node/.n8n-files/staging_ai/C22024.xlsx';\n\n// Validate Excel file path to prevent path traversal\nif (resolvedExcelFile.includes('..') || resolvedExcelFile.includes('~')) {\n throw new Error('Invalid Excel file path: path traversal detected');\n}\n\nif (!jwtTokenInput || !jwtTokenInput.startsWith('Bearer ')) {\n throw new Error('JWT Token is required and must start with \"Bearer \"');\n}\n\nconst BATCH_ID = (() => {\n // ใช้ Excel filename เป็น BATCH_ID เพื่อให้ checkpoint ทำงานถูกต้อง\n // เปลี่ยน Excel file = BATCH_ID ใหม่ = เริ่มใหม่ (ปลอดภัย)\n // ใช้ Excel file เดิม = BATCH_ID เดิม = resume ได้\n const filename = resolvedExcelFile.split('/').pop().split('\\\\').pop();\n const baseName = filename.replace(/\\.xlsx?$/i, '');\n return baseName + '-MIGRATION';\n})();\n\nconst CONFIG = {\n // Backend Settings\n BACKEND_URL: 'http://backend:3000',\n MIGRATION_TOKEN: jwtTokenInput,\n\n // Batch Settings\n BATCH_SIZE: batchSizeInput > 0 ? batchSizeInput : 10,\n BATCH_ID: BATCH_ID,\n DELAY_MS: 20000,\n\n // AI Job Settings (ADR-023A)\n AI_JOB_POLL_INTERVAL_MS: 5000,\n AI_JOB_TIMEOUT_MS: 120000,\n\n // Confidence Thresholds\n CONFIDENCE_HIGH: 0.85,\n CONFIDENCE_LOW: 0.60,\n\n // Paths (Container paths — ต้องตรงกับ volume mount ใน docker-compose)\n EXCEL_FILE: resolvedExcelFile,\n SOURCE_PDF_DIR: '/home/node/.n8n-files/staging_ai/Incoming/08C.2/2567',\n LOG_PATH: '/home/node/.n8n-files/migration_logs',\n};\n\nreturn [{ json: { config: CONFIG } }];"
|
|
},
|
|
"id": "65143f3f-b0ee-45e6-b0f9-bf57546b7482",
|
|
"name": "Set Configuration",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-9088,
|
|
4400
|
|
],
|
|
"notes": "กำหนดค่า Configuration ทั้งหมด — แก้ไขที่นี่เท่านั้น (ไม่มี Ollama config แล้ว)"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"url": "={{$('Set Configuration').first().json.config.BACKEND_URL}}/health",
|
|
"options": {
|
|
"timeout": 5000
|
|
}
|
|
},
|
|
"id": "b6295d67-9a34-4550-8e96-096a59a88053",
|
|
"name": "Check Backend Health",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.1,
|
|
"position": [
|
|
-8912,
|
|
4400
|
|
],
|
|
"notes": "ตรวจสอบ Backend พร้อมใช้งาน"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"url": "={{$('Set Configuration').first().json.config.BACKEND_URL}}/api/auth/profile",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": "={{$('Set Configuration').first().json.config.MIGRATION_TOKEN}}"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"timeout": 5000
|
|
}
|
|
},
|
|
"id": "92b9007e-9ad1-40f6-8e37-cec5113f6b30",
|
|
"name": "Validate Token",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.1,
|
|
"position": [
|
|
-8736,
|
|
4400
|
|
],
|
|
"notes": "FR-010a: ตรวจสอบ MIGRATION_TOKEN ก่อนเริ่ม Batch"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"url": "={{$('Set Configuration').first().json.config.BACKEND_URL}}/api/master/correspondence-types",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": "={{$('Set Configuration').first().json.config.MIGRATION_TOKEN}}"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"timeout": 10000
|
|
}
|
|
},
|
|
"id": "a9c11f42-b63b-412f-b919-88dbc7ab095e",
|
|
"name": "Fetch Categories",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.1,
|
|
"position": [
|
|
-9088,
|
|
4592
|
|
],
|
|
"notes": "ดึง Categories จาก Backend"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"url": "={{$('Set Configuration').first().json.config.BACKEND_URL}}/api/master/tags",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": "={{$('Set Configuration').first().json.config.MIGRATION_TOKEN}}"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"timeout": 10000
|
|
}
|
|
},
|
|
"id": "108423e3-90de-49a3-b950-8f78cd231b84",
|
|
"name": "Fetch Tags",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.1,
|
|
"position": [
|
|
-8912,
|
|
4592
|
|
],
|
|
"notes": "ดึง Tags ที่มีอยู่แล้วจาก Backend"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const fs = require('fs');\nconst config = $('Set Configuration').first().json.config;\n\ntry {\n if (!fs.existsSync(config.EXCEL_FILE)) {\n throw new Error(`Excel file not found at: ${config.EXCEL_FILE}`);\n }\n if (!fs.existsSync(config.SOURCE_PDF_DIR)) {\n throw new Error(`PDF Source directory not found at: ${config.SOURCE_PDF_DIR}`);\n }\n \n const files = fs.readdirSync(config.SOURCE_PDF_DIR);\n \n // Check write permission to log path\n fs.writeFileSync(`${config.LOG_PATH}/.preflight_ok`, new Date().toISOString());\n \n // Grab categories\n let categories = ['Correspondence','RFA','Drawing','Transmittal','Report','Other'];\n try {\n const upstreamData = $('Fetch Categories').first()?.json?.data;\n if (upstreamData && Array.isArray(upstreamData)) {\n categories = upstreamData.map(c => c.name || c.type || c);\n }\n } catch(e) {}\n \n // Grab existing tags\n let existingTags = [];\n try {\n const tagData = $('Fetch Tags').first()?.json?.data || [];\n existingTags = Array.isArray(tagData)\n ? tagData.map(t => ({ publicId: t.publicId, tagName: t.tag_name || t.name || '' })).filter(t => t.tagName && t.publicId)\n : [];\n } catch(e) {}\n \n return [{ json: { \n preflight_ok: true, \n pdf_count_in_source: files.length,\n excel_target: config.EXCEL_FILE,\n system_categories: categories,\n existing_tags: existingTags,\n timestamp: new Date().toISOString()\n }}];\n} catch (err) {\n throw new Error(`Pre-flight check failed: ${err.message}`);\n}"
|
|
},
|
|
"id": "d8e2efec-7fb4-44c3-b544-0dc51dcfd3fc",
|
|
"name": "File Mount Check",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-8736,
|
|
4592
|
|
],
|
|
"notes": "ตรวจสอบ File System + รวบรวม master data สำหรับ AI payload"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"fileSelector": "={{ $json.excel_target }}",
|
|
"options": {}
|
|
},
|
|
"id": "c2a6ca17-aae8-4213-93ab-8179e1febfb3",
|
|
"name": "Read Excel Binary",
|
|
"type": "n8n-nodes-base.readWriteFile",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
-9088,
|
|
4784
|
|
],
|
|
"notes": "ดึงไฟล์ Excel ขึ้นมาไว้ในหน่วยความจำ"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"options": {}
|
|
},
|
|
"id": "ebfdde99-92a1-4060-9867-2f330b13554f",
|
|
"name": "Read Excel",
|
|
"type": "n8n-nodes-base.spreadsheetFile",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-8912,
|
|
4784
|
|
],
|
|
"notes": "แปลงข้อมูล Excel เป็น JSON Data"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"url": "={{ $('Set Configuration').first().json.config.BACKEND_URL + '/api/ai/migration/checkpoint/' + $('Set Configuration').first().json.config.BATCH_ID }}",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": "={{$('Set Configuration').first().json.config.MIGRATION_TOKEN}}"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"timeout": 10000
|
|
}
|
|
},
|
|
"id": "9cf3971c-287a-42ab-9327-72e1a4d8011c",
|
|
"name": "Read Checkpoint",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.1,
|
|
"position": [
|
|
-8720,
|
|
4784
|
|
],
|
|
"alwaysOutputData": true,
|
|
"notes": "GET /api/ai/migration/checkpoint/:batchId — ADR-023A (ไม่ใช้ MySQL โดยตรง)"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const cpJson = $input.first()?.json || {};\nconst startIndex = cpJson.data?.lastProcessedIndex ?? cpJson.lastProcessedIndex ?? cpJson.data?.last_processed_index ?? cpJson.last_processed_index ?? 0;\nconst config = $('Set Configuration').first().json.config;\n\nconst allItems = $('Read Excel').all().map(i => i.json);\nconst remaining = allItems.slice(startIndex);\nconst currentBatch = remaining.slice(0, config.BATCH_SIZE);\n\n// Exit condition: ไม่มี records เหลือ\nif (currentBatch.length === 0) {\n return [{ json: { batch_complete: true, total_processed: startIndex } }];\n}\n\nconst normalize = (str) => {\n if (!str) return '';\n return String(str).normalize('NFC').trim();\n};\n\nreturn currentBatch.map((item, i) => {\n const getVal = (possibleKeys) => {\n const exactMatch = possibleKeys.find(k => item[k] !== undefined);\n if (exactMatch) return item[exactMatch];\n const lowerTrimmedKeys = Object.keys(item).map(k => ({ original: k, parsed: k.toLowerCase().trim() }));\n for (const pk of possibleKeys) {\n const found = lowerTrimmedKeys.find(k => k.parsed === pk.toLowerCase().trim());\n if (found) return item[found.original];\n }\n return '';\n };\n\n const docNum = getVal(['document_number', 'correspondence_number', 'Document Number', 'Corr. No.']);\n const excelFileName = getVal(['File name', 'file_name', 'File Name', 'filename']);\n \n if (!excelFileName) {\n throw new Error(`Missing 'File name' column for row ${i + startIndex + 1}, document: ${docNum}`);\n }\n \n return {\n json: {\n batch_complete: false,\n document_number: normalize(docNum),\n title: normalize(getVal(['title', 'Title', 'Subject', 'subject'])),\n legacy_number: normalize(getVal(['legacy_number', 'Legacy Number', 'Response Doc.'])),\n excel_revision: getVal(['revision', 'Revision', 'rev']) || 1,\n original_index: startIndex + i,\n batch_id: config.BATCH_ID,\n file_name: normalize(excelFileName),\n issued_date: normalize(getVal(['issued_date', 'Issued_date', 'Issued Date', 'date', 'Date'])),\n received_date: normalize(getVal(['received_date', 'Received_date', 'Received Date'])),\n sender: normalize(getVal(['sender', 'Sender', 'From', 'from'])),\n receiver: normalize(getVal(['receiver', 'Receiver', 'To', 'to'])),\n }\n };\n});"
|
|
},
|
|
"id": "c101543e-a3a2-4538-a99c-0419df50948f",
|
|
"name": "Process Batch + Encoding",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-8528,
|
|
4400
|
|
],
|
|
"alwaysOutputData": true,
|
|
"notes": "ตัด Batch + Normalize UTF-8 + Exit condition เมื่อ records หมด"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const fs = require('fs');\nconst path = require('path');\nconst config = $('Set Configuration').first().json.config;\n\nconst items = $input.all();\nif (!items || items.length === 0) return [];\n\n// ตรวจสอบ exit condition\nif (items[0]?.json?.batch_complete === true) {\n return items;\n}\n\nconst validated = [];\nconst errors = [];\n\nfor (const item of items) {\n const fileName = item.json?.file_name;\n if (!fileName) {\n errors.push({ ...item, json: { ...item.json, file_valid: false, error: 'file_name is missing', error_type: 'MISSING_FILENAME' } });\n continue;\n }\n \n let safeName = path.basename(String(fileName)).normalize('NFC');\n if (!safeName.toLowerCase().endsWith('.pdf')) safeName += '.pdf';\n const filePath = path.resolve(config.SOURCE_PDF_DIR, safeName);\n \n if (!filePath.startsWith(path.resolve(config.SOURCE_PDF_DIR))) {\n errors.push({ ...item, json: { ...item.json, file_valid: false, error: 'Path traversal detected', error_type: 'SECURITY' } });\n continue;\n }\n \n try {\n if (fs.existsSync(filePath)) {\n const stats = fs.statSync(filePath);\n validated.push({ ...item, json: { ...item.json, file_valid: true, file_path: filePath, file_size: stats.size } });\n } else {\n errors.push({ ...item, json: { ...item.json, file_valid: false, error: `File not found: ${safeName}`, error_type: 'FILE_NOT_FOUND' } });\n }\n } catch (err) {\n errors.push({ ...item, json: { ...item.json, file_valid: false, error: err.message, error_type: 'FILE_ERROR' } });\n }\n}\n\nreturn [...validated, ...errors];"
|
|
},
|
|
"id": "282e1c8a-74b6-4ae7-a50b-3e2ff7b1558d",
|
|
"name": "File Validator",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-8512,
|
|
4800
|
|
],
|
|
"notes": "ตรวจสอบไฟล์ PDF ใน Directory"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"fileSelector": "={{ $json.file_path }}",
|
|
"options": {}
|
|
},
|
|
"id": "ab2d3ff8-9862-4a5c-a21d-5e04004d6a40",
|
|
"name": "Read PDF File",
|
|
"type": "n8n-nodes-base.readWriteFile",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
-8288,
|
|
4640
|
|
],
|
|
"onError": "continueErrorOutput",
|
|
"notes": "อ่าน PDF Binary เพื่อเตรียม Upload"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "={{$('Set Configuration').first().json.config.BACKEND_URL}}/api/files/upload",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": "={{$('Set Configuration').first().json.config.MIGRATION_TOKEN}}"
|
|
},
|
|
{
|
|
"name": "Idempotency-Key",
|
|
"value": "={{ $json.batch_id + ':' + $json.document_number + ':upload' }}"
|
|
}
|
|
]
|
|
},
|
|
"sendBody": true,
|
|
"contentType": "multipart-form-data",
|
|
"bodyParameters": {
|
|
"parameters": [
|
|
{
|
|
"parameterType": "formBinaryData",
|
|
"name": "file",
|
|
"inputDataFieldName": "data"
|
|
}
|
|
]
|
|
},
|
|
"options": {
|
|
"timeout": 60000
|
|
}
|
|
},
|
|
"id": "0346a124-aaf3-49de-9181-77cb5e80a4d3",
|
|
"name": "Upload PDF to Backend",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.2,
|
|
"position": [
|
|
-8080,
|
|
4400
|
|
],
|
|
"onError": "continueErrorOutput",
|
|
"notes": "ADR-023A: อัพโหลด PDF เข้า Backend Temp Storage → รับ temp_attachment_id"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const config = $('Set Configuration').first().json.config;\nconst uploadResponse = $input.first()?.json || {};\nconst metaItem = $('File Validator').all()[$input.context?.itemIndex ?? 0]?.json || {};\nconst mountCheckData = $('File Mount Check').first()?.json || {};\n\nconst tempAttachmentPublicId = uploadResponse?.data?.publicId || uploadResponse?.publicId;\nconst tempAttachmentDbId = uploadResponse?.data?.id || uploadResponse?.id;\nif (!tempAttachmentPublicId) {\n throw new Error(`Upload failed — no temp_attachment_public_id returned for document: ${metaItem.document_number}`);\n}\n\n// สร้าง SubmitAiJobDto ตาม Backend DTO\nconst submitPayload = {\n type: 'migrate-document',\n payload: {\n tempAttachmentId: String(tempAttachmentPublicId),\n documentNumber: String(metaItem.document_number || ''),\n title: String(metaItem.title || ''),\n batchId: String(config.BATCH_ID),\n existingTags: mountCheckData.existing_tags || [],\n systemCategories: mountCheckData.system_categories || [],\n }\n};\n\n// Idempotency-Key: deterministic format ตาม FR-001a\nconst idempotencyKey = `${config.BATCH_ID}:${metaItem.document_number}`;\n\nreturn [{\n json: {\n ...metaItem,\n temp_attachment_public_id: tempAttachmentPublicId,\n temp_attachment_id: Number.isFinite(Number(tempAttachmentDbId)) ? Number(tempAttachmentDbId) : undefined,\n submit_payload: submitPayload,\n idempotency_key: idempotencyKey,\n }\n}];"
|
|
},
|
|
"id": "1b7e6adb-b7e2-40dd-b472-3a5a79861bfb",
|
|
"name": "Build AI Job Payload",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-7888,
|
|
4304
|
|
],
|
|
"notes": "สร้าง SubmitAiJobDto payload + Idempotency-Key ตาม FR-001a"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "={{$('Set Configuration').first().json.config.BACKEND_URL}}/api/ai/jobs",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": "={{$('Set Configuration').first().json.config.MIGRATION_TOKEN}}"
|
|
},
|
|
{
|
|
"name": "Idempotency-Key",
|
|
"value": "={{ $json.idempotency_key }}"
|
|
}
|
|
]
|
|
},
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ $json.submit_payload }}",
|
|
"options": {
|
|
"timeout": 30000
|
|
}
|
|
},
|
|
"id": "f2a5ee75-cd14-4118-925d-43182735aa75",
|
|
"name": "Submit AI Job",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.1,
|
|
"position": [
|
|
-7744,
|
|
4384
|
|
],
|
|
"onError": "continueErrorOutput",
|
|
"notes": "ADR-023A: POST /api/ai/jobs — Backend จัดการ Ollama ผ่าน BullMQ"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "// Poll GET /api/ai/jobs/{jobId} ทุก 5 วินาที จน status = completed หรือ timeout 120 วินาที\nconst config = $('Set Configuration').first().json.config;\nconst submitResponse = $input.first()?.json || {};\nconst originalMeta = $('Build AI Job Payload').all()[$input.context?.itemIndex ?? 0]?.json || {};\n\nconst jobId = submitResponse?.jobId || submitResponse?.data?.jobId;\nif (!jobId) {\n // FR-010b: ถ้า 401 → mark TOKEN_EXPIRED\n if (submitResponse?.statusCode === 401 || submitResponse?.error?.status === 401) {\n throw new Error('TOKEN_EXPIRED: 401 Unauthorized — กรุณา renew MIGRATION_TOKEN แล้ว resume');\n }\n throw new Error(`Submit AI Job failed — no jobId returned for: ${originalMeta.document_number}`);\n}\n\nconst pollIntervalMs = config.AI_JOB_POLL_INTERVAL_MS || 5000;\nconst timeoutMs = config.AI_JOB_TIMEOUT_MS || 120000;\nconst startTime = Date.now();\n\nwhile (true) {\n if (Date.now() - startTime > timeoutMs) {\n throw new Error(`AI Job timeout after ${timeoutMs}ms for jobId: ${jobId}, document: ${originalMeta.document_number}`);\n }\n \n const statusResponse = await $helpers.httpRequest({\n method: 'GET',\n url: `${config.BACKEND_URL}/api/ai/jobs/${jobId}`,\n headers: { 'Authorization': config.MIGRATION_TOKEN },\n json: true\n });\n \n const status = statusResponse?.status || statusResponse?.data?.status;\n \n if (status === 'completed') {\n return [{\n json: {\n ...originalMeta,\n job_id: jobId,\n ai_result: statusResponse?.result || statusResponse?.data?.result || {},\n ai_status: 'completed',\n }\n }];\n }\n \n if (status === 'failed') {\n return [{\n json: {\n ...originalMeta,\n job_id: jobId,\n ai_status: 'failed',\n parse_error: `AI Job failed: ${JSON.stringify(statusResponse?.error || statusResponse?.data?.error || 'unknown')}`,\n error_type: 'AI_JOB_FAILED',\n route_index: 3\n }\n }];\n }\n \n // status = 'processing' — รอแล้ว poll ใหม่\n await new Promise(resolve => setTimeout(resolve, pollIntervalMs));\n}"
|
|
},
|
|
"id": "f95bbb5f-6bde-4146-b577-8d824776029e",
|
|
"name": "Poll AI Job Status",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-7552,
|
|
4368
|
|
],
|
|
"notes": "Poll GET /api/ai/jobs/{jobId} ทุก 5 วินาที (timeout 120 วินาที)"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const config = $('Set Configuration').first().json.config;\nconst items = $input.all();\nconst results = [];\n\nconst CATEGORY_TO_TYPE_CODE = {\n 'Correspondence': 'LETTER',\n 'RFA': 'RFA',\n 'Transmittal': 'TRANSMITTAL',\n 'Drawing': 'OTHER',\n 'Report': 'OTHER',\n 'Other': 'OTHER',\n};\n\nfor (const item of items) {\n const data = item.json;\n \n // ถ้า poll ส่ง error กลับมาโดยตรง (route_index = 3)\n if (data.route_index === 3 || data.ai_status === 'failed') {\n results.push({ json: { ...data, route_index: 3 } });\n continue;\n }\n \n const ai = data.ai_result || {};\n \n if (!ai || typeof ai !== 'object') {\n results.push({ json: { ...data, parse_error: 'Empty or invalid ai_result', error_type: 'PARSE_ERROR', route_index: 3 } });\n continue;\n }\n \n // Enum Validation for Category\n const systemCategories = ['Correspondence','RFA','Drawing','Transmittal','Report','Other'];\n let finalCategory = ai.suggested_category || ai.category || 'Correspondence';\n if (!systemCategories.includes(finalCategory)) {\n finalCategory = String(data.document_number || '').includes('-RFA-') ? 'RFA' : 'Correspondence';\n }\n const typeCode = CATEGORY_TO_TYPE_CODE[finalCategory] || 'LETTER';\n \n // Tag normalization — is_new flag\n const suggestedTags = Array.isArray(ai.suggested_tags)\n ? ai.suggested_tags.map(t => ({\n tagName: String(t.tagName || t.tag_name || ''),\n publicId: t.publicId || t.public_id || undefined,\n isNew: Boolean(t.isNew ?? t.is_new ?? !t.publicId),\n colorCode: t.colorCode || t.color_code || 'default',\n })).filter(t => t.tagName)\n : [];\n \n const confidence = Number(ai.confidence || 0);\n \n const normalizedAi = {\n ...ai,\n suggested_category: finalCategory,\n type_code: typeCode,\n confidence,\n suggested_tags: suggestedTags,\n };\n \n let route_index;\n let review_reason = '';\n let reject_reason = '';\n \n if (confidence >= config.CONFIDENCE_HIGH && ai.is_valid !== false) {\n route_index = 0; // Auto Ready\n } else if (confidence >= config.CONFIDENCE_LOW) {\n route_index = 1; // Flagged\n review_reason = `Confidence ${confidence.toFixed(2)} < ${config.CONFIDENCE_HIGH}`;\n } else {\n route_index = 2; // Rejected\n reject_reason = ai.is_valid === false ? 'AI marked invalid' : `Confidence ${confidence.toFixed(2)} < ${config.CONFIDENCE_LOW}`;\n }\n \n results.push({\n json: {\n ...data,\n ai_result: normalizedAi,\n route_index,\n review_reason,\n reject_reason,\n }\n });\n}\n\nreturn results;"
|
|
},
|
|
"id": "6bdcd7fd-0a8a-4f21-91bd-ed5fcbda860c",
|
|
"name": "Parse & Validate AI Response",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-7376,
|
|
4368
|
|
],
|
|
"notes": "Parse AI result + Confidence routing prep + Tag normalization (isNew flag)"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 2
|
|
},
|
|
"conditions": [
|
|
{
|
|
"leftValue": "={{ $json.route_index }}",
|
|
"rightValue": 0,
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "equals",
|
|
"singleValue": true
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "Auto Ready"
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 2
|
|
},
|
|
"conditions": [
|
|
{
|
|
"leftValue": "={{ $json.route_index }}",
|
|
"rightValue": 1,
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "equals",
|
|
"singleValue": true
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "Review Flagged"
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 2
|
|
},
|
|
"conditions": [
|
|
{
|
|
"leftValue": "={{ $json.route_index }}",
|
|
"rightValue": 2,
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "equals",
|
|
"singleValue": true
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "Rejected"
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 2
|
|
},
|
|
"conditions": [
|
|
{
|
|
"leftValue": "={{ $json.route_index }}",
|
|
"rightValue": 3,
|
|
"operator": {
|
|
"type": "number",
|
|
"operation": "equals",
|
|
"singleValue": true
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "Error Log"
|
|
}
|
|
]
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "35ad90cd-bb71-44a6-8851-ae4ea6ed4747",
|
|
"name": "Route by Confidence",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
-7216,
|
|
4512
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "={{$('Set Configuration').first().json.config.BACKEND_URL}}/api/ai/migration/queue/record",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": "={{$('Set Configuration').first().json.config.MIGRATION_TOKEN}}"
|
|
},
|
|
{
|
|
"name": "Idempotency-Key",
|
|
"value": "={{ $json.idempotency_key + ':queue' }}"
|
|
}
|
|
]
|
|
},
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ batchId: $json.batch_id, documentNumber: $json.document_number, subject: ($json.ai_result?.subject || $json.title), originalSubject: $json.title, tempAttachmentId: $json.temp_attachment_id === undefined ? undefined : Number($json.temp_attachment_id), confidence: $json.ai_result?.confidence, reviewReason: $json.review_reason || '', status: 'PENDING', aiResult: $json.ai_result, idempotencyKey: $json.batch_id + ':' + $json.document_number }) }}",
|
|
"options": {
|
|
"timeout": 10000
|
|
}
|
|
},
|
|
"id": "f26c6512-8f2d-4cca-a643-65ae7255c37a",
|
|
"name": "Insert Review Queue (Auto)",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.1,
|
|
"position": [
|
|
-6976,
|
|
4496
|
|
],
|
|
"onError": "continueErrorOutput",
|
|
"notes": "POST /api/ai/migration/queue/record (PENDING) — ADR-023A"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "={{$('Set Configuration').first().json.config.BACKEND_URL}}/api/ai/migration/queue/record",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": "={{$('Set Configuration').first().json.config.MIGRATION_TOKEN}}"
|
|
},
|
|
{
|
|
"name": "Idempotency-Key",
|
|
"value": "={{ $json.idempotency_key + ':queue' }}"
|
|
}
|
|
]
|
|
},
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ batchId: $json.batch_id, documentNumber: $json.document_number, subject: ($json.ai_result?.subject || $json.title), originalSubject: $json.title, tempAttachmentId: $json.temp_attachment_id === undefined ? undefined : Number($json.temp_attachment_id), confidence: $json.ai_result?.confidence, reviewReason: $json.review_reason || '', status: 'PENDING_REVIEW', aiResult: $json.ai_result, idempotencyKey: $json.batch_id + ':' + $json.document_number }) }}",
|
|
"options": {
|
|
"timeout": 10000
|
|
}
|
|
},
|
|
"id": "b720ce71-d0f4-402a-9bd3-077cf1730977",
|
|
"name": "Insert Review Queue (Flagged)",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.1,
|
|
"position": [
|
|
-6944,
|
|
4720
|
|
],
|
|
"onError": "continueErrorOutput",
|
|
"notes": "POST /api/ai/migration/queue/record (PENDING_REVIEW) — ADR-023A"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const fs = require('fs');\nconst item = $input.first();\nconst config = $('Set Configuration').first().json.config;\n\nconst csvPath = `${config.LOG_PATH}/reject_log_${config.BATCH_ID}.csv`;\nconst header = 'timestamp,document_number,title,reject_reason,ai_confidence,job_id\\n';\nconst esc = (s) => `\"${String(s || '').replace(/\"/g, '\"\"')}\"`;\n\nif (!fs.existsSync(csvPath)) {\n fs.writeFileSync(csvPath, header, 'utf8');\n}\n\nconst line = [\n new Date().toISOString(),\n esc(item.json.document_number),\n esc(item.json.title),\n esc(item.json.reject_reason),\n item.json.ai_result?.confidence ?? 'N/A',\n esc(item.json.job_id || '')\n].join(',') + '\\n';\n\nfs.appendFileSync(csvPath, line, 'utf8');\n\nreturn [$input.first()];"
|
|
},
|
|
"id": "6e83de99-1244-4478-9a33-e779cdb9504a",
|
|
"name": "Log Reject to CSV",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-6944,
|
|
4848
|
|
],
|
|
"notes": "บันทึกรายการที่ถูกปฏิเสธลง CSV"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"jsCode": "const fs = require('fs');\nconst items = $input.all();\nconst config = $('Set Configuration').first().json.config;\n\nconst csvPath = `${config.LOG_PATH}/error_log_${config.BATCH_ID}.csv`;\nconst header = 'timestamp,document_number,error_type,error_message,job_id\\n';\nconst esc = (s) => `\"${String(s || '').replace(/\"/g, '\"\"')}\"`;\n\nif (!fs.existsSync(csvPath)) {\n fs.writeFileSync(csvPath, header, 'utf8');\n}\n\nfor (const item of items) {\n const line = [\n new Date().toISOString(),\n esc(item.json.document_number),\n esc(item.json.error_type || 'UNKNOWN'),\n esc(item.json.error || item.json.parse_error),\n esc(item.json.job_id || '')\n ].join(',') + '\\n';\n fs.appendFileSync(csvPath, line, 'utf8');\n}\n\nreturn items;"
|
|
},
|
|
"id": "511428dc-3aad-4de1-a9dc-9a87c791371e",
|
|
"name": "Log Error to CSV",
|
|
"type": "n8n-nodes-base.code",
|
|
"typeVersion": 2,
|
|
"position": [
|
|
-7936,
|
|
4800
|
|
],
|
|
"notes": "บันทึก Error ลง CSV"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "={{$('Set Configuration').first().json.config.BACKEND_URL}}/api/ai/migration/errors",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": "={{$('Set Configuration').first().json.config.MIGRATION_TOKEN}}"
|
|
},
|
|
{
|
|
"name": "Idempotency-Key",
|
|
"value": "={{ ($json.batch_id || $('Set Configuration').first().json.config.BATCH_ID) + ':' + ($json.document_number || 'WORKFLOW') + ':error' }}"
|
|
}
|
|
]
|
|
},
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ batchId: $('Set Configuration').first().json.config.BATCH_ID, documentNumber: $json.document_number || 'WORKFLOW', errorType: $json.error_type || 'UNKNOWN', errorMessage: $json.error || $json.parse_error || $json.message || '', jobId: $json.job_id || '' }) }}",
|
|
"options": {
|
|
"timeout": 10000
|
|
}
|
|
},
|
|
"id": "7e8f3617-a6e4-4d40-922d-eb93ae91e690",
|
|
"name": "Log Error to DB",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.1,
|
|
"position": [
|
|
-7232,
|
|
4944
|
|
],
|
|
"onError": "continueErrorOutput",
|
|
"notes": "POST /api/ai/migration/errors — ADR-023A"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "={{$('Set Configuration').first().json.config.BACKEND_URL}}/api/ai/migration/checkpoint",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": "={{$('Set Configuration').first().json.config.MIGRATION_TOKEN}}"
|
|
},
|
|
{
|
|
"name": "Idempotency-Key",
|
|
"value": "={{ $('Set Configuration').first().json.config.BATCH_ID + ':checkpoint:' + (($json.original_index || 0) + 1) }}"
|
|
}
|
|
]
|
|
},
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ batchId: $('Set Configuration').first().json.config.BATCH_ID, lastProcessedIndex: ($json.original_index || 0) + 1, status: 'RUNNING' }) }}",
|
|
"options": {
|
|
"timeout": 10000
|
|
}
|
|
},
|
|
"id": "9471990c-1abe-42f8-8062-bd68cb9ad985",
|
|
"name": "Save Checkpoint",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.1,
|
|
"position": [
|
|
-6784,
|
|
4640
|
|
],
|
|
"onError": "continueErrorOutput",
|
|
"notes": "POST /api/ai/migration/checkpoint — ADR-023A"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"amount": "={{$('Set Configuration').first().json.config.DELAY_MS / 1000}}",
|
|
"unit": "seconds"
|
|
},
|
|
"id": "fcf3e098-93f6-42ee-b930-aa0bc84d3ed7",
|
|
"name": "Delay",
|
|
"type": "n8n-nodes-base.wait",
|
|
"typeVersion": 1,
|
|
"position": [
|
|
-6640,
|
|
4880
|
|
],
|
|
"webhookId": "38e97a99-4dcc-4b63-977a-a02945a1c369",
|
|
"notes": "หน่วงเวลาระหว่าง Records"
|
|
},
|
|
{
|
|
"parameters": {
|
|
"rules": {
|
|
"values": [
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 2
|
|
},
|
|
"conditions": [
|
|
{
|
|
"leftValue": "={{ $json.batch_complete }}",
|
|
"rightValue": true,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals",
|
|
"singleValue": true
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "Batch Complete"
|
|
},
|
|
{
|
|
"conditions": {
|
|
"options": {
|
|
"caseSensitive": true,
|
|
"leftValue": "",
|
|
"typeValidation": "strict",
|
|
"version": 2
|
|
},
|
|
"conditions": [
|
|
{
|
|
"leftValue": "={{ $json.batch_complete }}",
|
|
"rightValue": false,
|
|
"operator": {
|
|
"type": "boolean",
|
|
"operation": "equals",
|
|
"singleValue": true
|
|
}
|
|
}
|
|
],
|
|
"combinator": "and"
|
|
},
|
|
"renameOutput": true,
|
|
"outputKey": "Continue Loop"
|
|
}
|
|
]
|
|
},
|
|
"options": {}
|
|
},
|
|
"id": "e489d28d-37e8-4204-bba8-07a5226b1275",
|
|
"name": "Check Batch Complete",
|
|
"type": "n8n-nodes-base.switch",
|
|
"typeVersion": 3.2,
|
|
"position": [
|
|
-8512,
|
|
4608
|
|
]
|
|
},
|
|
{
|
|
"parameters": {
|
|
"method": "POST",
|
|
"url": "={{$('Set Configuration').first().json.config.BACKEND_URL}}/api/ai/migration/checkpoint",
|
|
"sendHeaders": true,
|
|
"headerParameters": {
|
|
"parameters": [
|
|
{
|
|
"name": "Authorization",
|
|
"value": "={{$('Set Configuration').first().json.config.MIGRATION_TOKEN}}"
|
|
},
|
|
{
|
|
"name": "Idempotency-Key",
|
|
"value": "={{ $('Set Configuration').first().json.config.BATCH_ID + ':complete' }}"
|
|
}
|
|
]
|
|
},
|
|
"sendBody": true,
|
|
"specifyBody": "json",
|
|
"jsonBody": "={{ JSON.stringify({ batchId: $('Set Configuration').first().json.config.BATCH_ID, lastProcessedIndex: $json.total_processed, status: 'COMPLETED' }) }}",
|
|
"options": {
|
|
"timeout": 10000
|
|
}
|
|
},
|
|
"id": "171b627f-3b00-4fbf-86b7-6076fdc29d19",
|
|
"name": "Mark Batch Complete",
|
|
"type": "n8n-nodes-base.httpRequest",
|
|
"typeVersion": 4.1,
|
|
"position": [
|
|
-8304,
|
|
4400
|
|
],
|
|
"notes": "Update checkpoint status to COMPLETED when batch finishes"
|
|
}
|
|
],
|
|
"pinData": {},
|
|
"connections": {
|
|
"Form Trigger": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Set Configuration",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Set Configuration": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check Backend Health",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check Backend Health": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Validate Token",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Validate Token": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Fetch Categories",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Fetch Categories": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Fetch Tags",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Fetch Tags": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "File Mount Check",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"File Mount Check": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Read Excel Binary",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Read Excel Binary": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Read Excel",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Read Excel": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Read Checkpoint",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Read Checkpoint": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Process Batch + Encoding",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Process Batch + Encoding": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Check Batch Complete",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"File Validator": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Read PDF File",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Read PDF File": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Upload PDF to Backend",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Log Error to CSV",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Upload PDF to Backend": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Build AI Job Payload",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Log Error to CSV",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Build AI Job Payload": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Submit AI Job",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Submit AI Job": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Poll AI Job Status",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Log Error to CSV",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Poll AI Job Status": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Parse & Validate AI Response",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Parse & Validate AI Response": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Route by Confidence",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Route by Confidence": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Insert Review Queue (Auto)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Insert Review Queue (Flagged)",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Log Reject to CSV",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Log Error to CSV",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Insert Review Queue (Auto)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Save Checkpoint",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Log Error to CSV",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Insert Review Queue (Flagged)": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Save Checkpoint",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Log Error to CSV",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Save Checkpoint": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Delay",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Log Error to CSV",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Delay": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Read Checkpoint",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Check Batch Complete": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Mark Batch Complete",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "File Validator",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Log Reject to CSV": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Delay",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Log Error to CSV": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Log Error to DB",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
},
|
|
"Log Error to DB": {
|
|
"main": [
|
|
[
|
|
{
|
|
"node": "Delay",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
],
|
|
[
|
|
{
|
|
"node": "Delay",
|
|
"type": "main",
|
|
"index": 0
|
|
}
|
|
]
|
|
]
|
|
}
|
|
},
|
|
"active": false,
|
|
"settings": {
|
|
"executionOrder": "v1",
|
|
"binaryMode": "separate"
|
|
},
|
|
"versionId": "efadd20e-a46b-4354-8f75-ec1de215d065",
|
|
"meta": {
|
|
"templateCredsSetupCompleted": true,
|
|
"instanceId": "9e70e47c1eaf3bac72f497ddfbde0983f840f7d0f059537f7e37dd70de18ecb7"
|
|
},
|
|
"id": "4LlPbAKU5BZLgiTg",
|
|
"tags": [
|
|
{
|
|
"updatedAt": "2026-05-23T12:26:47.389Z",
|
|
"createdAt": "2026-05-23T12:26:47.389Z",
|
|
"id": "jNSEtctPbU5leFPw",
|
|
"name": "v2"
|
|
},
|
|
{
|
|
"updatedAt": "2026-05-23T12:26:47.393Z",
|
|
"createdAt": "2026-05-23T12:26:47.393Z",
|
|
"id": "mGZTyPfxbcsAuFpR",
|
|
"name": "migration"
|
|
}
|
|
]
|
|
} |