{ "name": "LCBP3 Migration Workflow v1.8.0", "nodes": [ { "parameters": {}, "id": "c41e7a06-5115-48e8-a8ce-821bb3e4d2dc", "name": "Manual Trigger", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [ 4640, 3696 ], "notes": "กดรันด้วยตนเอง" }, { "parameters": { "jsCode": "// ============================================\n// CONFIGURATION - แก้ไขค่าที่นี่\n// ============================================\nconst CONFIG = {\n // Ollama Settings\n OLLAMA_HOST: 'http://192.168.20.100:11434',\n OLLAMA_MODEL_PRIMARY: 'llama3.2:3b',\n OLLAMA_MODEL_FALLBACK: 'mistral:7b-instruct-q4_K_M',\n \n // Backend Settings\n BACKEND_URL: 'https://backend.np-dms.work',\n MIGRATION_TOKEN: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im1pZ3JhdGlvbl9ib3QiLCJzdWIiOjUsInNjb3BlIjoiR2xvYmFsIiwiaWF0IjoxNzcyNzc0MzI5LCJleHAiOjQ5Mjg1MzQzMjl9.TtA8zoHy7G9J5jPgYQPv7yw-9X--B_hl-Nv-c9V4PaA',\n \n // Batch Settings\n BATCH_SIZE: 10,\n BATCH_ID: 'migration_20260226',\n DELAY_MS: 2000,\n \n // Thresholds\n CONFIDENCE_HIGH: 0.85,\n CONFIDENCE_LOW: 0.60,\n MAX_RETRY: 3,\n FALLBACK_THRESHOLD: 5,\n \n // Source Definitions - แก้ไขโฟลเดอร์และไฟล์ทำงานที่นี่\n EXCEL_FILE: '/home/node/.n8n-files/staging_ai/C22024.xlsx',\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 // Database\n DB_HOST: '192.168.10.8',\n DB_PORT: 3306,\n DB_NAME: 'lcbp3',\n DB_USER: 'migration_bot',\n DB_PASSWORD: 'Center2025'\n};\n\nreturn [{ json: { config_loaded: true, timestamp: new Date().toISOString(), config: CONFIG } }];" }, "id": "bc8c9b9d-284d-4ce5-b7ff-d5b4bb36e748", "name": "Set Configuration", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 4832, 3696 ], "notes": "กำหนดค่า Configuration ทั้งหมด - แก้ไขที่นี่ก่อนรัน" }, { "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": "ccb5fea4-773d-4584-a14c-88845f4c2bc3", "name": "Fetch Categories", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, "position": [ 5040, 3696 ], "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": "f1a2b3c4-d5e6-7f8g-9h0i-j1k2l3m4n5o6", "name": "Fetch Tags", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, "position": [ 5040, 3856 ], "notes": "ดึง Tags ที่มีอยู่แล้วจาก Backend" }, { "parameters": { "url": "={{$('Set Configuration').first().json.config.BACKEND_URL}}/health", "options": { "timeout": 5000 } }, "id": "0fe2cc93-7d88-4290-8170-2863e087afd3", "name": "Check Backend Health", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, "position": [ 5008, 3328 ], "onError": "continueErrorOutput", "notes": "ตรวจสอบ Backend พร้อมใช้งาน" }, { "parameters": { "jsCode": "const fs = require('fs');\nconst config = $('Set Configuration').first().json.config;\n\n// Check file mount and inputs\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 out of the previous node (Fetch Categories) if available\n // otherwise use fallback array\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); // very loose mapping depending on API response\n }\n } catch(e) {}\n \n // Grab existing tags from Fetch Tags node\n let existingTags = [];\n try {\n const tagData = $('Fetch Tags').first()?.json?.data || [];\n existingTags = Array.isArray(tagData) ? tagData.map(t => t.tag_name || t.name || '').filter(Boolean) : [];\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": "5bdb31ca-9588-404d-92ce-3438bdd9835b", "name": "File Mount Check", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 5248, 3392 ], "notes": "ตรวจสอบ File System มีไฟล์ Excel และ Folder ตามตั้งค่า" }, { "parameters": { "operation": "executeQuery", "query": "SELECT last_processed_index, status FROM migration_progress WHERE batch_id = '{{$('Set Configuration').first().json.config.BATCH_ID}}' LIMIT 1", "options": {} }, "id": "2907a4ca-2a46-45ef-8920-9684d00ffda7", "name": "Read Checkpoint", "type": "n8n-nodes-base.mySql", "typeVersion": 2.4, "position": [ 5504, 3376 ], "alwaysOutputData": true, "credentials": { "mySql": { "id": "CHHfbKhMacNo03V4", "name": "MySQL account" } }, "onError": "continueErrorOutput", "notes": "อ่านตำแหน่งล่าสุดที่ประมวลผล" }, { "parameters": { "fileSelector": "={{ $json.excel_target }}", "options": {} }, "id": "f035d28b-413b-4386-bbef-d242cd22aa8f", "name": "Read Excel Binary", "type": "n8n-nodes-base.readWriteFile", "typeVersion": 1, "position": [ 5040, 4112 ], "notes": "ดึงไฟล์ Excel ขึ้นมาไว้ในหน่วยความจำ" }, { "parameters": { "options": {} }, "id": "35727d79-e3c2-4fdf-8bc3-064914393cf7", "name": "Read Excel", "type": "n8n-nodes-base.spreadsheetFile", "typeVersion": 2, "position": [ 5264, 3968 ], "notes": "แปลงข้อมูล Excel เป็น JSON Data" }, { "parameters": { "jsCode": "const cpJson = $input.first()?.json || {};\nconst startIndex = 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// Encoding Normalization\nconst normalize = (str) => {\n if (!str) return '';\n return String(str).normalize('NFC').trim();\n};\n\nreturn currentBatch.map((item, i) => {\n const docNum = item.document_number || item['Document Number'] || item['Corr. No.'];\n // Use File name from Excel directly - must exist\n const excelFileName = item['File name'] || item.file_name || item['File Name'] || item.filename;\n if (!excelFileName) {\n throw new Error(`Missing 'File name' column for row ${i + startIndex + 1}, document: ${docNum}`);\n }\n const fileName = normalize(excelFileName);\n return {\n json: {\n document_number: normalize(docNum),\n title: normalize(item.title || item.Title || item['Subject']),\n legacy_number: normalize(item.legacy_number || item['Legacy Number'] || item['Response Doc.'] || ''),\n excel_revision: item.revision || item.Revision || item.rev || 1,\n original_index: startIndex + i,\n batch_id: config.BATCH_ID,\n file_name: fileName\n }\n };\n});" }, "id": "49c98c75-456b-4a1d-a203-a5b2bf19fd15", "name": "Process Batch + Encoding", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 5712, 3360 ], "alwaysOutputData": true, "notes": "ตัด Batch + Normalize UTF-8" }, { "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\nconst validated = [];\nconst errors = [];\n\nfor (const item of items) {\n const fileName = item.json?.file_name;\n if (!fileName) {\n errors.push({\n ...item,\n json: { ...item.json, file_valid: false, error: 'file_name is missing', error_type: 'MISSING_FILENAME', file_exists: false }\n });\n continue;\n }\n \n // Use file name from Excel directly, add .pdf if missing\n let safeName = path.basename(String(fileName)).normalize('NFC');\n if (!safeName.toLowerCase().endsWith('.pdf')) {\n safeName += '.pdf';\n }\n const filePath = path.resolve(config.SOURCE_PDF_DIR, safeName);\n \n // Path traversal check\n if (!filePath.startsWith(path.resolve(config.SOURCE_PDF_DIR))) {\n errors.push({\n ...item,\n json: { ...item.json, file_valid: false, error: 'Path traversal detected', error_type: 'SECURITY', file_exists: false }\n });\n continue;\n }\n \n try {\n if (fs.existsSync(filePath)) {\n const stats = fs.statSync(filePath);\n validated.push({\n ...item,\n json: { ...item.json, file_valid: true, file_exists: true, file_size: stats.size, file_path: filePath }\n });\n } else {\n errors.push({\n ...item,\n json: { ...item.json, file_valid: false, error: `File not found: ${safeName}`, error_type: 'FILE_NOT_FOUND', file_exists: false }\n });\n }\n } catch (err) {\n errors.push({\n ...item,\n json: { ...item.json, file_valid: false, error: err.message, error_type: 'FILE_ERROR', file_exists: false }\n });\n }\n}\n\nreturn [...validated, ...errors];" }, "id": "51e91c88-98cd-4df4-81ac-e452b25e5c06", "name": "File Validator", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 5904, 3264 ], "notes": "ตรวจสอบไฟล์ PDF ตัวชี้ใน Directory จาก Config" }, { "parameters": { "operation": "executeQuery", "query": "SELECT is_fallback_active, recent_error_count FROM migration_fallback_state WHERE batch_id = '{{$('Set Configuration').first().json.config.BATCH_ID}}' LIMIT 1", "options": {} }, "id": "88c205b6-9b94-4a4f-ad53-ab3cad6fde27", "name": "Check Fallback State", "type": "n8n-nodes-base.mySql", "typeVersion": 2.4, "position": [ 6032, 3488 ], "alwaysOutputData": true, "credentials": { "mySql": { "id": "CHHfbKhMacNo03V4", "name": "MySQL account" } }, "onError": "continueErrorOutput", "notes": "ตรวจสอบว่าต้องใช้ Fallback Model หรือไม่" }, { "parameters": { "jsCode": "const config = $('Set Configuration').first().json.config;\nconst fallbackState = $input.first().json[0] || { is_fallback_active: false, recent_error_count: 0 };\n\nconst isFallback = fallbackState.is_fallback_active || false;\nconst model = isFallback ? config.OLLAMA_MODEL_FALLBACK : config.OLLAMA_MODEL_PRIMARY;\n\n// Safely pull categories from the first Check node\nlet systemCategories = ['Correspondence','RFA','Drawing','Transmittal','Report','Other'];\ntry { systemCategories = $('File Mount Check').first().json.system_categories || systemCategories; } catch (e) {}\n\nconst items = $('File Validator').all();\n\nreturn items.map(item => {\n const systemPrompt = `You are a Document Controller for a large construction project.\nYour task is to validate document metadata and suggest relevant tags.\nYou MUST respond ONLY with valid JSON. No explanation, no markdown, no extra text.\nIf there are no issues, \"detected_issues\" must be an empty array [].`;\n\n const userPrompt = `Validate this document metadata and respond in JSON:\n\nDocument Number: ${item.json.document_number}\nTitle: ${item.json.title}\nExpected Pattern: [ORG]-[TYPE]-[SEQ] e.g. \"TCC-COR-0001\"\nCategory List (MUST match system enum exactly): ${JSON.stringify(systemCategories)}\n\nAnalyze the document and suggest relevant tags based on:\n1. Document content/title keywords (e.g., \"Foundation\", \"Structure\", \"Electrical\", \"Safety\")\n2. Document type indicators (e.g., \"Drawing\", \"Report\", \"Inspection\")\n3. Organization codes present in document number\n4. Any discipline or phase indicators\n\nRespond ONLY with this exact JSON structure:\n{\n \"is_valid\": true | false,\n \"confidence\": 0.0 to 1.0,\n \"suggested_category\": \"\",\n \"detected_issues\": [\"\"],\n \"suggested_title\": \"\",\n \"suggested_tags\": [\"\", \"\"],\n \"tag_confidence\": 0.0 to 1.0\n}`;\n\n return {\n json: {\n ...item.json,\n active_model: model,\n is_fallback: isFallback,\n system_categories: systemCategories,\n ollama_payload: {\n model: model,\n prompt: `${systemPrompt}\\n\\n${userPrompt}`,\n stream: false,\n format: 'json'\n }\n }\n };\n});" }, "id": "9f82950f-7533-4cbd-8e1e-8e441c1cb2a5", "name": "Build AI Prompt", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 6032, 3696 ], "notes": "สร้าง Prompt โดยใช้ Categories จาก System" }, { "parameters": { "method": "POST", "url": "={{$('Set Configuration').first().json.config.OLLAMA_HOST}}/api/generate", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ $json.ollama_payload }}", "options": { "timeout": 120000 } }, "id": "ae9b6be5-284c-44db-b7f0-b4839a59230e", "name": "Ollama AI Analysis", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, "position": [ 6240, 3696 ], "notes": "เรียก Ollama วิเคราะห์เอกสาร" }, { "parameters": { "jsCode": "const items = $input.all();\nconst parsed = [];\nconst parseErrors = [];\n\nfor (const item of items) {\n try {\n let raw = item.json.response || '';\n \n // Clean markdown\n raw = raw.replace(/```json/gi, '').replace(/```/g, '').trim();\n const result = JSON.parse(raw);\n \n // Schema Validation\n if (typeof result.is_valid !== 'boolean') throw new Error('is_valid must be boolean');\n if (typeof result.confidence !== 'number' || result.confidence < 0 || result.confidence > 1) {\n throw new Error('confidence must be float 0.0-1.0');\n }\n if (!Array.isArray(result.detected_issues)) throw new Error('detected_issues must be array');\n \n // Tag Validation - ensure suggested_tags is an array\n if (!Array.isArray(result.suggested_tags)) {\n result.suggested_tags = [];\n }\n // Normalize tags: trim, lowercase, remove duplicates\n result.suggested_tags = [...new Set(result.suggested_tags.map(t => String(t).trim()).filter(t => t.length > 0))];\n \n // Tag confidence validation\n if (typeof result.tag_confidence !== 'number' || result.tag_confidence < 0 || result.tag_confidence > 1) {\n result.tag_confidence = 0.5; // default if missing or invalid\n }\n \n // Enum Validation\n const systemCategories = item.json.system_categories || [];\n if (!systemCategories.includes(result.suggested_category)) {\n throw new Error(`Category \"${result.suggested_category}\" not in system enum`);\n }\n \n parsed.push({\n ...item,\n json: { ...item.json, ai_result: result, parse_error: null }\n });\n } catch (err) {\n parseErrors.push({\n ...item,\n json: {\n ...item.json,\n ai_result: null,\n parse_error: err.message,\n raw_ai_response: item.json.response,\n error_type: 'AI_PARSE_ERROR'\n }\n });\n }\n}\n\nreturn [parsed.length > 0 ? parsed : [{ json: { _placeholder: true } }], parseErrors.length > 0 ? parseErrors : [{ json: { _placeholder: true } }]];" }, "id": "281dc950-a3b6-4412-a0b4-76663b8c37ea", "name": "Parse & Validate AI Response", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 6432, 3696 ], "notes": "Parse JSON + Validate Schema + Enum Check" }, { "parameters": { "operation": "executeQuery", "query": "INSERT INTO migration_fallback_state (batch_id, recent_error_count, is_fallback_active) VALUES ('{{$('Set Configuration').first().json.config.BATCH_ID}}', 1, FALSE) ON DUPLICATE KEY UPDATE recent_error_count = recent_error_count + 1, is_fallback_active = CASE WHEN recent_error_count + 1 >= {{$('Set Configuration').first().json.config.FALLBACK_THRESHOLD}} THEN TRUE ELSE is_fallback_active END, updated_at = NOW()", "options": {} }, "id": "41904cb6-b6f3-4a32-9dd5-c44e8e0cefab", "name": "Update Fallback State", "type": "n8n-nodes-base.mySql", "typeVersion": 2.4, "position": [ 6640, 3888 ], "credentials": { "mySql": { "id": "CHHfbKhMacNo03V4", "name": "MySQL account" } }, "notes": "เพิ่ม Error count และตรวจสอบ Fallback threshold" }, { "parameters": { "jsCode": "const config = $('Set Configuration').first().json.config;\nconst items = $('Parse & Validate AI Response').all();\n\nconst autoIngest = [];\nconst reviewQueue = [];\nconst rejectLog = [];\nconst errorLog = [];\n\nfor (const item of items) {\n if (item.json.parse_error || !item.json.ai_result) {\n errorLog.push(item);\n continue;\n }\n \n const ai = item.json.ai_result;\n \n // Revision Drift Protection (ถ้ามีข้อมูลจาก DB)\n if (item.json.current_db_revision !== undefined) {\n const expectedRev = item.json.current_db_revision + 1;\n if (parseInt(item.json.excel_revision) !== expectedRev) {\n reviewQueue.push({\n ...item,\n json: { ...item.json, review_reason: `Revision drift: Excel=${item.json.excel_revision}, Expected=${expectedRev}` }\n });\n continue;\n }\n }\n \n // Confidence Routing\n if (ai.confidence >= config.CONFIDENCE_HIGH && ai.is_valid === true) {\n autoIngest.push(item);\n } else if (ai.confidence >= config.CONFIDENCE_LOW) {\n reviewQueue.push({\n ...item,\n json: { ...item.json, review_reason: `Confidence ${ai.confidence.toFixed(2)} < ${config.CONFIDENCE_HIGH}` }\n });\n } else {\n rejectLog.push({\n ...item,\n json: { ...item.json, reject_reason: ai.is_valid === false ? 'AI marked invalid' : `Confidence ${ai.confidence.toFixed(2)} < ${config.CONFIDENCE_LOW}` }\n });\n }\n}\n\n// Output 0: Auto, 1: Review, 2: Reject, 3: Error\nreturn [autoIngest, reviewQueue, rejectLog, errorLog];" }, "id": "897dfc43-9f4f-4a9b-8727-64f3483ac56a", "name": "Confidence Router", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 6640, 3696 ], "notes": "แยกตาม Confidence: Auto(≥0.85) / Review(≥0.60) / Reject(<0.60)" }, { "parameters": { "method": "POST", "url": "={{$('Set Configuration').first().json.config.BACKEND_URL}}/api/migration/import", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Authorization", "value": "={{$('Set Configuration').first().json.config.MIGRATION_TOKEN}}" }, { "name": "Idempotency-Key", "value": "={{$json.document_number}}:{{$('Set Configuration').first().json.config.BATCH_ID}}" } ] }, "sendBody": true, "specifyBody": "json", "jsonBody": "={\n \"document_number\": \"{{$json.document_number}}\",\n \"title\": \"{{$json.ai_result.suggested_title || $json.title}}\",\n \"category\": \"{{$json.ai_result.suggested_category}}\",\n \"source_file_path\": \"{{$json.file_path}}\",\n \"ai_confidence\": {{$json.ai_result.confidence}},\n \"ai_issues\": {{JSON.stringify($json.ai_result.detected_issues)}},\n \"ai_tags\": {{JSON.stringify($json.ai_result.suggested_tags || [])}},\n \"tag_confidence\": {{$json.ai_result.tag_confidence || 0}},\n \"migrated_by\": \"SYSTEM_IMPORT\",\n \"batch_id\": \"{{$('Set Configuration').first().json.config.BATCH_ID}}\",\n \"details\": {\n \"legacy_number\": \"{{$json.legacy_number}}\"\n }\n}", "options": { "timeout": 30000 } }, "id": "49762c5d-0cb3-4acf-97f7-7e22905148dc", "name": "Import to Backend", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.1, "position": [ 6832, 3488 ], "notes": "ส่งข้อมูลเข้า LCBP3 Backend พร้อม Idempotency-Key" }, { "parameters": { "jsCode": "const item = $input.first();\nconst shouldCheckpoint = item.json.original_index % 10 === 0;\n\nreturn [{\n json: {\n ...item.json,\n should_update_checkpoint: shouldCheckpoint,\n checkpoint_index: item.json.original_index,\n import_status: 'success',\n timestamp: new Date().toISOString()\n }\n}];" }, "id": "7fd03017-f08c-4e93-9486-36069f91ce57", "name": "Flag Checkpoint", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 7040, 3488 ], "notes": "กำหนดว่าจะบันทึก Checkpoint หรือไม่ (ทุก 10 records)" }, { "parameters": { "operation": "executeQuery", "query": "INSERT INTO migration_progress (batch_id, last_processed_index, status) VALUES ('{{$('Set Configuration').first().json.config.BATCH_ID}}', {{$json.checkpoint_index}}, 'RUNNING') ON DUPLICATE KEY UPDATE last_processed_index = {{$json.checkpoint_index}}, updated_at = NOW()", "options": {} }, "id": "27b26d7b-2b57-479f-81ca-8d9319a45a7d", "name": "Save Checkpoint", "type": "n8n-nodes-base.mySql", "typeVersion": 2.4, "position": [ 7232, 3488 ], "credentials": { "mySql": { "id": "CHHfbKhMacNo03V4", "name": "MySQL account" } }, "notes": "บันทึกความคืบหน้าลง Database" }, { "parameters": { "operation": "executeQuery", "query": "INSERT INTO migration_review_queue (document_number, title, original_title, ai_suggested_category, ai_confidence, ai_issues, review_reason, status, created_at) VALUES ('{{$json.document_number}}', '{{$json.ai_result.suggested_title || $json.title}}', '{{$json.title}}', '{{$json.ai_result.suggested_category}}', {{$json.ai_result.confidence}}, '{{JSON.stringify($json.ai_result.detected_issues)}}', '{{$json.review_reason}}', 'PENDING', NOW()) ON DUPLICATE KEY UPDATE status = 'PENDING', review_reason = '{{$json.review_reason}}', created_at = NOW()", "options": {} }, "id": "5d9547a9-36c8-434d-93e2-405be47d4e43", "name": "Insert Review Queue", "type": "n8n-nodes-base.mySql", "typeVersion": 2.4, "position": [ 6832, 3696 ], "credentials": { "mySql": { "id": "CHHfbKhMacNo03V4", "name": "MySQL account" } }, "notes": "บันทึกรายการที่ต้องตรวจสอบโดยคน (ไม่สร้าง Correspondence)" }, { "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.csv`;\nconst header = 'timestamp,document_number,title,reject_reason,ai_confidence,ai_issues\\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(JSON.stringify(item.json.ai_result?.detected_issues || []))\n].join(',') + '\\n';\n\nfs.appendFileSync(csvPath, line, 'utf8');\n\nreturn [$input.first()];" }, "id": "e933dc6a-885c-4607-916f-f28c655ceac4", "name": "Log Reject to CSV", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 6832, 3888 ], "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.csv`;\nconst header = 'timestamp,document_number,error_type,error_message,raw_ai_response\\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.raw_ai_response || '')\n ].join(',') + '\\n';\n \n fs.appendFileSync(csvPath, line, 'utf8');\n}\n\nreturn items;" }, "id": "cda3d253-a14d-4ec5-adaa-3e7b276be1f2", "name": "Log Error to CSV", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 6032, 4096 ], "notes": "บันทึก Error ลง CSV (จาก File Validator)" }, { "parameters": { "operation": "executeQuery", "query": "INSERT INTO migration_errors (batch_id, document_number, error_type, error_message, raw_ai_response, created_at) VALUES ('{{$('Set Configuration').first().json.config.BATCH_ID}}', '{{$json.document_number}}', '{{$json.error_type || \"UNKNOWN\"}}', '{{$json.error || $json.parse_error}}', '{{$json.raw_ai_response || \"\"}}', NOW())", "options": {} }, "id": "1ee11a28-e339-42ac-9066-0ff6dac30920", "name": "Log Error to DB", "type": "n8n-nodes-base.mySql", "typeVersion": 2.4, "position": [ 6640, 4096 ], "credentials": { "mySql": { "id": "CHHfbKhMacNo03V4", "name": "MySQL account" } }, "notes": "บันทึก Error ลง MariaDB" }, { "parameters": { "amount": "={{$('Set Configuration').first().json.config.DELAY_MS}}", "unit": "milliseconds" }, "id": "0bd637f6-6260-44ab-a27e-d7f4cb372ce4", "name": "Delay", "type": "n8n-nodes-base.wait", "typeVersion": 1, "position": [ 7440, 3696 ], "webhookId": "38e97a99-4dcc-4b63-977a-a02945a1c369", "notes": "หน่วงเวลาระหว่าง Batches" } ], "pinData": {}, "connections": { "Manual 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": "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": "File Validator", "type": "main", "index": 0 } ] ] }, "File Validator": { "main": [ [ { "node": "Check Fallback State", "type": "main", "index": 0 } ] ] }, "Check Fallback State": { "main": [ [ { "node": "Build AI Prompt", "type": "main", "index": 0 } ] ] }, "Build AI Prompt": { "main": [ [ { "node": "Ollama AI Analysis", "type": "main", "index": 0 } ] ] }, "Ollama AI Analysis": { "main": [ [ { "node": "Parse & Validate AI Response", "type": "main", "index": 0 } ] ] }, "Parse & Validate AI Response": { "main": [ [ { "node": "Confidence Router", "type": "main", "index": 0 } ] ] }, "Confidence Router": { "main": [ [ { "node": "Import to Backend", "type": "main", "index": 0 } ] ] }, "Insert Review Queue": { "main": [ [ { "node": "Delay", "type": "main", "index": 0 } ] ] }, "Log Reject to CSV": { "main": [ [ { "node": "Delay", "type": "main", "index": 0 } ] ] }, "Import to Backend": { "main": [ [ { "node": "Flag Checkpoint", "type": "main", "index": 0 } ] ] }, "Flag Checkpoint": { "main": [ [ { "node": "Save Checkpoint", "type": "main", "index": 0 } ] ] }, "Save Checkpoint": { "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 } ] ] }, "Delay": { "main": [ [ { "node": "Read Checkpoint", "type": "main", "index": 0 } ] ] } }, "active": false, "settings": { "executionOrder": "v1", "availableInMCP": false }, "versionId": "c52d7a07-398b-495e-b384-fb4f02ef3fed", "meta": { "templateCredsSetupCompleted": true, "instanceId": "9e70e47c1eaf3bac72f497ddfbde0983f840f7d0f059537f7e37dd70de18ecb7" }, "id": "u7CLP05AyFb8Um0P", "tags": [] }