260307:1319 20260307:1300 fix n8n workflow project_id, issued_date and received_date
Build and Deploy / deploy (push) Successful in 4m11s
Build and Deploy / deploy (push) Successful in 4m11s
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
const fs = require('fs');
|
||||
const filepath = 'd:/nap-dms.lcbp3/specs/03-Data-and-Storage/n8n.workflow.json';
|
||||
const workflow = JSON.parse(fs.readFileSync(filepath, 'utf8'));
|
||||
|
||||
const switchNodeIndex = workflow.nodes.findIndex(n => n.name === 'Route by Confidence');
|
||||
|
||||
if (switchNodeIndex > -1) {
|
||||
workflow.nodes[switchNodeIndex] = {
|
||||
id: "23d11b5e-49b4-4b53-911b-76b6bb77aab8",
|
||||
name: "Route by Confidence",
|
||||
type: "n8n-nodes-base.switch",
|
||||
typeVersion: 3.2,
|
||||
position: [6840, 3696],
|
||||
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 Ingest"
|
||||
},
|
||||
{
|
||||
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 Queue"
|
||||
},
|
||||
{
|
||||
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: "Reject"
|
||||
},
|
||||
{
|
||||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (workflow.connections['Confidence Router'] || workflow.connections['Route by Confidence']) {
|
||||
workflow.connections['Route by Confidence'] = {
|
||||
'main': [
|
||||
[ { 'node': 'Import to Backend', 'type': 'main', 'index': 0 } ],
|
||||
[ { 'node': 'Insert Review Queue', 'type': 'main', 'index': 0 } ],
|
||||
[ { 'node': 'Log Reject to CSV', 'type': 'main', 'index': 0 } ],
|
||||
[ { 'node': 'Log Error to CSV', 'type': 'main', 'index': 0 } ]
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
fs.writeFileSync(filepath, JSON.stringify(workflow, null, 2));
|
||||
console.log("Updated Switch node to use typeVersion 3.2 properly.");
|
||||
} else {
|
||||
console.log("Could not find Route by Confidence node.");
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "1.5.1",
|
||||
"description": "",
|
||||
"description": "<p align=\"center\">\r <a href=\"http://nestjs.com/\" target=\"blank\"><img src=\"https://nestjs.com/img/logo-small.svg\" width=\"120\" alt=\"Nest Logo\" /></a>\r </p>",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
@@ -135,5 +135,11 @@
|
||||
"overrides": {
|
||||
"fast-xml-parser": "^5.3.5"
|
||||
}
|
||||
}
|
||||
},
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
"test": "test"
|
||||
},
|
||||
"keywords": [],
|
||||
"type": "commonjs"
|
||||
}
|
||||
|
||||
@@ -41,4 +41,20 @@ export class ImportCorrespondenceDto {
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
details?: Record<string, any>;
|
||||
|
||||
@IsNumber()
|
||||
@IsNotEmpty()
|
||||
project_id!: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
issued_date?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
received_date?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
body?: string;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
},
|
||||
{
|
||||
"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 } }];"
|
||||
"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: 5,\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 PROJECT_ID: 1\n};\n\nreturn [{ json: { config_loaded: true, timestamp: new Date().toISOString(), config: CONFIG } }];"
|
||||
},
|
||||
"id": "bc8c9b9d-284d-4ce5-b7ff-d5b4bb36e748",
|
||||
"name": "Set Configuration",
|
||||
@@ -166,7 +166,7 @@
|
||||
},
|
||||
{
|
||||
"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});"
|
||||
"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 issued_date: normalize(item.issued_date || item['Issued Date'] || item.date || item.Date || item.document_date || item.Document_Date),\n received_date: normalize(item.received_date || item['Received Date'] || item.receive || item.Receive)\n }\n };\n});"
|
||||
},
|
||||
"id": "49c98c75-456b-4a1d-a203-a5b2bf19fd15",
|
||||
"name": "Process Batch + Encoding",
|
||||
@@ -219,7 +219,7 @@
|
||||
},
|
||||
{
|
||||
"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\": \"<one from Category List>\",\n \"detected_issues\": [\"<issue1>\"],\n \"suggested_title\": \"<corrected title or null>\",\n \"suggested_tags\": [\"<tag1>\", \"<tag2>\"],\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});"
|
||||
"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 = $('Extract PDF Text').all();\n\nreturn items.map(item => {\n const docNum = String(item.json.document_number || '');\n const isRFA = docNum.includes('-RFA-') || String(item.json.title || '').toLowerCase().includes('rfa');\n \n const systemPrompt = `You are an expert Document Controller for a large construction project (LCBP3).\nYour task is to classify documents and extract precise metadata for the Document Management System.\nYou MUST respond ONLY with valid JSON. No explanation, no markdown.`;\n\n const pdfText = String(item.json.text || '').substring(0, 3000).replace(/[^a-zA-Z0-9ก-๙\\s\\./]/g, ' ');\n const userPrompt = `Analyze this document metadata:\nDocument Number: ${item.json.document_number}\nTitle: ${item.json.title}\nLegacy Number: ${item.json.legacy_number}\n\nRules:\n1. Category must be one of: ${JSON.stringify(systemCategories)}\n2. If Document Number contains \"-RFA-\", suggest_category MUST be \"RFA\".\n3. For RFA, extract:\n - \"ref_no\": Reference number if mentioned in title/legacy.\n - \"response_to\": If this is a response to another document.\n4. For Letters (Correspondence), identify:\n - \"from_org\": Sending organization (e.g., TCC, CNNC).\n - \"to_org\": Receiving organization.\n\nRespond ONLY with this JSON structure:\n{\n \"is_valid\": true,\n \"confidence\": 0.95,\n \"suggested_category\": \"${isRFA ? 'RFA' : 'Correspondence'}\",\n \"detected_issues\": [],\n \"suggested_title\": null,\n \"suggested_tags\": [\"Construction\", \"${isRFA ? 'Request' : 'Letter'}\"],\n \"metadata\": {\n \"ref_no\": null,\n \"response_to\": null,\n \"from_org\": null,\n \"to_org\": null,\n \"body\": null\n }\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",
|
||||
@@ -254,7 +254,7 @@
|
||||
},
|
||||
{
|
||||
"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 } }]];"
|
||||
"jsCode": "const items = $input.all();\nconst results = [];\n\nfor (const item of items) {\n try {\n let raw = item.json.response || '';\n \n // Clean markdown and whitespace\n raw = raw.replace(/```json/gi, '').replace(/```/g, '').trim();\n if (!raw) throw new Error('Empty response from AI');\n\n const result = JSON.parse(raw);\n \n // Metadata mapping & normalization\n const meta = result.metadata || {};\n const metadata = {\n ref_no: String(meta.ref_no || '').trim() || null,\n response_to: String(meta.response_to || '').trim() || null,\n from_org: String(meta.from_org || '').trim() || null,\n to_org: String(meta.to_org || '').trim() || null,\n body: String(result.body || meta.body || '').trim() || null\n };\n \n // Tag Validation\n let tags = Array.isArray(result.suggested_tags) ? result.suggested_tags : [];\n tags = [...new Set(tags.map(t => String(t).trim()).filter(t => t.length > 0))];\n \n // Enum Validation for Category\n const systemCategories = item.json.system_categories || [];\n let finalCategory = result.suggested_category;\n if (!systemCategories.includes(finalCategory)) {\n finalCategory = String(item.json.document_number || '').includes('-RFA-') ? 'RFA' : 'Correspondence';\n }\n \n const d_issued = item.json.issued_date || null;\n const d_received = item.json.received_date || d_issued;\n results.push({\n json: { \n ...item.json, \n ai_result: { ...result, suggested_category: finalCategory, suggested_tags: tags, body: result.body || meta.body || null }, \n metadata: metadata,\n issued_date: d_issued,\n received_date: d_received,\n parse_error: null \n }\n });\n } catch (err) {\n results.push({\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 results;"
|
||||
},
|
||||
"id": "281dc950-a3b6-4412-a0b4-76663b8c37ea",
|
||||
"name": "Parse & Validate AI Response",
|
||||
@@ -290,7 +290,7 @@
|
||||
},
|
||||
{
|
||||
"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];"
|
||||
"jsCode": "const config = $('Set Configuration').first().json.config;\nconst items = $('Parse & Validate AI Response').all();\n\nconst results = [];\n\nfor (const item of items) {\n const data = item.json;\n \n // Base structure ensuring we keep all existing data\n let resultItem = { json: { ...data } };\n \n // Handle Parse Errors from upstream\n if (data.parse_error || !data.ai_result) {\n resultItem.json.route_index = 3;\n results.push(resultItem);\n continue;\n }\n \n const ai = data.ai_result;\n \n // Revision Drift Protection\n if (data.current_db_revision !== undefined) {\n const expectedRev = data.current_db_revision + 1;\n if (parseInt(data.excel_revision) !== expectedRev) {\n resultItem.json.review_reason = `Revision drift: Excel=${data.excel_revision}, Expected=${expectedRev}`;\n resultItem.json.route_index = 1;\n results.push(resultItem);\n continue;\n }\n }\n \n // Confidence Routing\n if (ai.confidence >= config.CONFIDENCE_HIGH && ai.is_valid === true) {\n resultItem.json.route_index = 0;\n } else if (ai.confidence >= config.CONFIDENCE_LOW) {\n resultItem.json.review_reason = `Confidence ${ai.confidence.toFixed(2)} < ${config.CONFIDENCE_HIGH}`;\n resultItem.json.route_index = 1;\n } else {\n resultItem.json.reject_reason = ai.is_valid === false ? 'AI marked invalid' : `Confidence ${ai.confidence.toFixed(2)} < ${config.CONFIDENCE_LOW}`;\n resultItem.json.route_index = 2;\n }\n results.push(resultItem);\n}\n\nreturn results;"
|
||||
},
|
||||
"id": "897dfc43-9f4f-4a9b-8727-64f3483ac56a",
|
||||
"name": "Confidence Router",
|
||||
@@ -321,7 +321,7 @@
|
||||
},
|
||||
"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}",
|
||||
"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 \"migrated_by\": \"SYSTEM_IMPORT\",\n \"batch_id\": \"{{$('Set Configuration').first().json.config.BATCH_ID}}\",\n \"project_id\": {{$('Set Configuration').first().json.config.PROJECT_ID}},\n \"issued_date\": \"{{$json.issued_date}}\",\n \"received_date\": \"{{$json.received_date}}\",\n \"body\": {{JSON.stringify($json.ai_result.body || \"\")}},\n \"details\": {\n \"legacy_number\": \"{{$json.legacy_number}}\",\n \"ref_no\": \"{{$json.metadata.ref_no}}\",\n \"response_to\": \"{{$json.metadata.response_to}}\",\n \"from_org\": \"{{$json.metadata.from_org}}\",\n \"to_org\": \"{{$json.metadata.to_org}}\"\n }\n}",
|
||||
"options": {
|
||||
"timeout": 30000
|
||||
}
|
||||
@@ -459,6 +459,149 @@
|
||||
],
|
||||
"webhookId": "38e97a99-4dcc-4b63-977a-a02945a1c369",
|
||||
"notes": "หน่วงเวลาระหว่าง Batches"
|
||||
},
|
||||
{
|
||||
"id": "23d11b5e-49b4-4b53-911b-76b6bb77aab8",
|
||||
"name": "Route by Confidence",
|
||||
"type": "n8n-nodes-base.switch",
|
||||
"typeVersion": 3.2,
|
||||
"position": [
|
||||
6840,
|
||||
3696
|
||||
],
|
||||
"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 Ingest"
|
||||
},
|
||||
{
|
||||
"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 Queue"
|
||||
},
|
||||
{
|
||||
"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": "Reject"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fileSelector": "={{ $json.file_path }}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "node-read-pdf-1",
|
||||
"name": "Read PDF File",
|
||||
"type": "n8n-nodes-base.readWriteFile",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
5904,
|
||||
3064
|
||||
],
|
||||
"onError": "continueErrorOutput"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "pdf",
|
||||
"binaryPropertyName": "data",
|
||||
"options": {}
|
||||
},
|
||||
"id": "node-extract-pdf-1",
|
||||
"name": "Extract PDF Text",
|
||||
"type": "n8n-nodes-base.extractFromFile",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
6032,
|
||||
3064
|
||||
],
|
||||
"onError": "continueErrorOutput"
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
@@ -577,7 +720,7 @@
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Check Fallback State",
|
||||
"node": "Read PDF File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
@@ -632,7 +775,7 @@
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Import to Backend",
|
||||
"node": "Route by Confidence",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
@@ -726,6 +869,60 @@
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Route by Confidence": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Import to Backend",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Insert Review Queue",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Log Reject to CSV",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Log Error to CSV",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Read PDF File": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Extract PDF Text",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Extract PDF Text": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Check Fallback State",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
@@ -740,4 +937,4 @@
|
||||
},
|
||||
"id": "u7CLP05AyFb8Um0P",
|
||||
"tags": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,887 @@
|
||||
{
|
||||
"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: 5,\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 docNum = String(item.json.document_number || '');\n const isRFA = docNum.includes('-RFA-') || String(item.json.title || '').toLowerCase().includes('rfa');\n \n const systemPrompt = `You are an expert Document Controller for a large construction project (LCBP3).\nYour task is to classify documents and extract precise metadata for the Document Management System.\nYou MUST respond ONLY with valid JSON. No explanation, no markdown.`;\n\n const userPrompt = `Analyze this document metadata:\nDocument Number: ${item.json.document_number}\nTitle: ${item.json.title}\nLegacy Number: ${item.json.legacy_number}\n\nRules:\n1. Category must be one of: ${JSON.stringify(systemCategories)}\n2. If Document Number contains \"-RFA-\", suggest_category MUST be \"RFA\".\n3. For RFA, extract:\n - \"ref_no\": Reference number if mentioned in title/legacy.\n - \"response_to\": If this is a response to another document.\n4. For Letters (Correspondence), identify:\n - \"from_org\": Sending organization (e.g., TCC, CNNC).\n - \"to_org\": Receiving organization.\n\nRespond ONLY with this JSON structure:\n{\n \"is_valid\": true,\n \"confidence\": 0.95,\n \"suggested_category\": \"${isRFA ? 'RFA' : 'Correspondence'}\",\n \"detected_issues\": [],\n \"suggested_title\": null,\n \"suggested_tags\": [\"Construction\", \"${isRFA ? 'Request' : 'Letter'}\"],\n \"metadata\": {\n \"ref_no\": null,\n \"response_to\": null,\n \"from_org\": null,\n \"to_org\": null\n }\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 results = [];\n\nfor (const item of items) {\n try {\n let raw = item.json.response || '';\n \n // Clean markdown and whitespace\n raw = raw.replace(/```json/gi, '').replace(/```/g, '').trim();\n if (!raw) throw new Error('Empty response from AI');\n\n const result = JSON.parse(raw);\n \n // Metadata mapping & normalization\n const meta = result.metadata || {};\n const metadata = {\n ref_no: String(meta.ref_no || '').trim() || null,\n response_to: String(meta.response_to || '').trim() || null,\n from_org: String(meta.from_org || '').trim() || null,\n to_org: String(meta.to_org || '').trim() || null\n };\n \n // Tag Validation\n let tags = Array.isArray(result.suggested_tags) ? result.suggested_tags : [];\n tags = [...new Set(tags.map(t => String(t).trim()).filter(t => t.length > 0))];\n \n // Enum Validation for Category\n const systemCategories = item.json.system_categories || [];\n let finalCategory = result.suggested_category;\n if (!systemCategories.includes(finalCategory)) {\n finalCategory = String(item.json.document_number || '').includes('-RFA-') ? 'RFA' : 'Correspondence';\n }\n \n results.push({\n json: { \n ...item.json, \n ai_result: { ...result, suggested_category: finalCategory, suggested_tags: tags }, \n metadata: metadata,\n parse_error: null \n }\n });\n } catch (err) {\n results.push({\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 results;"
|
||||
},
|
||||
"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 results = [];\n\nfor (const item of items) {\n const data = item.json;\n \n // Base structure ensuring we keep all existing data\n let resultItem = { json: { ...data } };\n \n // Handle Parse Errors from upstream\n if (data.parse_error || !data.ai_result) {\n resultItem.json.route_index = 3;\n results.push(resultItem);\n continue;\n }\n \n const ai = data.ai_result;\n \n // Revision Drift Protection\n if (data.current_db_revision !== undefined) {\n const expectedRev = data.current_db_revision + 1;\n if (parseInt(data.excel_revision) !== expectedRev) {\n resultItem.json.review_reason = `Revision drift: Excel=${data.excel_revision}, Expected=${expectedRev}`;\n resultItem.json.route_index = 1;\n results.push(resultItem);\n continue;\n }\n }\n \n // Confidence Routing\n if (ai.confidence >= config.CONFIDENCE_HIGH && ai.is_valid === true) {\n resultItem.json.route_index = 0;\n } else if (ai.confidence >= config.CONFIDENCE_LOW) {\n resultItem.json.review_reason = `Confidence ${ai.confidence.toFixed(2)} < ${config.CONFIDENCE_HIGH}`;\n resultItem.json.route_index = 1;\n } else {\n resultItem.json.reject_reason = ai.is_valid === false ? 'AI marked invalid' : `Confidence ${ai.confidence.toFixed(2)} < ${config.CONFIDENCE_LOW}`;\n resultItem.json.route_index = 2;\n }\n results.push(resultItem);\n}\n\nreturn results;"
|
||||
},
|
||||
"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 \"migrated_by\": \"SYSTEM_IMPORT\",\n \"batch_id\": \"{{$('Set Configuration').first().json.config.BATCH_ID}}\",\n \"details\": {\n \"legacy_number\": \"{{$json.legacy_number}}\",\n \"ref_no\": \"{{$json.metadata.ref_no}}\",\n \"response_to\": \"{{$json.metadata.response_to}}\",\n \"from_org\": \"{{$json.metadata.from_org}}\",\n \"to_org\": \"{{$json.metadata.to_org}}\"\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"
|
||||
},
|
||||
{
|
||||
"id": "23d11b5e-49b4-4b53-911b-76b6bb77aab8",
|
||||
"name": "Route by Confidence",
|
||||
"type": "n8n-nodes-base.switch",
|
||||
"typeVersion": 3.2,
|
||||
"position": [
|
||||
6840,
|
||||
3696
|
||||
],
|
||||
"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 Ingest"
|
||||
},
|
||||
{
|
||||
"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 Queue"
|
||||
},
|
||||
{
|
||||
"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": "Reject"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"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": "Route by Confidence",
|
||||
"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
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Route by Confidence": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Import to Backend",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Insert Review Queue",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Log Reject to CSV",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Log Error to CSV",
|
||||
"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": []
|
||||
}
|
||||
Reference in New Issue
Block a user