8
n8n 中文网amn8n.com

n8n_3_CAD-BIM-批量转换器-管道

高级

这是一个Document Extraction, Multimodal AI领域的自动化工作流,包含 82 个节点。主要使用 If, Set, Code, Merge, ManualTrigger 等节点。 批量转换 CAD/BIM 文件为 XLSX/DAE,带验证和报告

前置要求
  • OpenAI API Key
  • Anthropic API Key
  • Google Gemini API Key
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "id": "u5MdPlYjhriENe3R",
  "meta": {
    "instanceId": "faa70e11b7175129a74fd834d3451fdc1862589b16d68ded03f91ca7b1ecca12"
  },
  "name": "n8n_3_CAD-BIM-批量转换器-管道",
  "tags": [],
  "nodes": [
    {
      "id": "be5c0de2-0df1-4c35-8b0e-26f8d1911bda",
      "name": "便签 23",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1088,
        224
      ],
      "parameters": {
        "width": 376,
        "height": 368,
        "content": "## 📋 快速入门指南"
      },
      "typeVersion": 1
    },
    {
      "id": "d8a6fce0-af86-4769-8911-afb6c665f38d",
      "name": "便签25",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1088,
        608
      ],
      "parameters": {
        "color": 2,
        "width": 380,
        "height": 224,
        "content": "## 🎓 视频教程"
      },
      "typeVersion": 1
    },
    {
      "id": "1182ee14-cf89-45ee-96dc-d96a6864dcfa",
      "name": "便签 28",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1088,
        1168
      ],
      "parameters": {
        "color": 2,
        "width": 372,
        "height": 244,
        "content": "## 🆘 支持"
      },
      "typeVersion": 1
    },
    {
      "id": "8bd00e43-cb87-489a-87b6-a6258262a5bf",
      "name": "便签 29",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1088,
        848
      ],
      "parameters": {
        "color": 2,
        "width": 380,
        "height": 304,
        "content": "## 🎯 使用案例"
      },
      "typeVersion": 1
    },
    {
      "id": "3ccefd7c-ade0-4125-952c-928115a49ebf",
      "name": "便签 31",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1088,
        112
      ],
      "parameters": {
        "width": 372,
        "height": 100,
        "content": "⭐ **如果您觉得我们的工具有用**,请考虑在 [GitHub](https://github.com/datadrivenconstruction/cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto) 上为我们的仓库加星。**您的支持帮助我们**改进并继续为社区开发开放解决方案!"
      },
      "typeVersion": 1
    },
    {
      "id": "c5388249-6dc3-4070-9057-2b5668251832",
      "name": "便签",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -688,
        112
      ],
      "parameters": {
        "width": 520,
        "height": 1940,
        "content": "## 📋 选项参数配置指南"
      },
      "typeVersion": 1
    },
    {
      "id": "2fe7360d-df50-4eb7-8799-441e7aed7a1a",
      "name": "手动触发器",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -80,
        704
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "614d8fe1-6c80-4cb0-b941-5aa3998a2321",
      "name": "捕获管道开始时间 1",
      "type": "n8n-nodes-base.code",
      "position": [
        144,
        656
      ],
      "parameters": {
        "jsCode": "// Capture pipeline start time at the very beginning\nconst now = new Date();\nreturn [{\n  json: {\n    pipeline_start_time: now.toISOString(),\n    pipeline_start_timestamp: now.getTime(),\n    pipeline_start_message: `Pipeline started at ${now.toLocaleString()}`\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "e64288d1-9d53-4036-afde-437c9892a927",
      "name": "合并管道开始与配置 1",
      "type": "n8n-nodes-base.merge",
      "position": [
        688,
        672
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3
    },
    {
      "id": "7b214d29-a619-4a41-8cfa-67f074470b89",
      "name": "设置配置参数 1",
      "type": "n8n-nodes-base.set",
      "position": [
        416,
        512
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "converter-path",
              "name": "converter_path",
              "type": "string",
              "value": "C:\\Users\\Artem Boiko\\Desktop\\n8n\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto-main\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto-main\\DDC_Converter_Revit\\datadrivenlibs\\RvtExporter.exe"
            },
            {
              "id": "source-folder",
              "name": "source_folder",
              "type": "string",
              "value": "C:\\Users\\Artem Boiko\\Desktop\\n8n\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto-main\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto-main\\Sample_Projects"
            },
            {
              "id": "output-folder",
              "name": "output_folder",
              "type": "string",
              "value": "C:\\Users\\Artem Boiko\\Desktop\\n8n\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto-main\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto-main\\Sample_Projects"
            },
            {
              "id": "include-subfolders",
              "name": "include_subfolders",
              "type": "boolean",
              "value": false
            },
            {
              "id": "file-extension",
              "name": "file_extension",
              "type": "string",
              "value": ".rvt"
            },
            {
              "id": "a811f4ba-b7a5-4774-a1fa-90d9c43a0de6",
              "name": "options",
              "type": "string",
              "value": "basic schedule  -no-collada"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "53648934-0bb7-49d3-83f9-ab4280bb3ec9",
      "name": "查找 CAD 文件 1",
      "type": "n8n-nodes-base.executeCommand",
      "position": [
        848,
        688
      ],
      "parameters": {
        "command": "=powershell -Command \"Get-ChildItem -LiteralPath '{{ $json.source_folder }}' -Filter '*{{ $json.file_extension }}' {{ $json.include_subfolders ? '-Recurse' : '' }} | Select-Object -ExpandProperty FullName\""
      },
      "typeVersion": 1
    },
    {
      "id": "894a4fae-cd2f-4418-b35b-fc6a06cda820",
      "name": "合并配置与搜索结果 1",
      "type": "n8n-nodes-base.merge",
      "position": [
        1008,
        576
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3
    },
    {
      "id": "cc6f17d1-4393-4c77-947a-53c42c01543c",
      "name": "处理文件列表 1",
      "type": "n8n-nodes-base.code",
      "position": [
        1168,
        576
      ],
      "parameters": {
        "jsCode": "// Process file list with configuration and pipeline start time\nconst configData = $input.first().json || {};\nconst searchData = $input.last().json || {};\n\nconst output = searchData.stdout || '';\nconst stderr = searchData.stderr || '';\n\n// Preserve pipeline start time\nconst pipeline_start_time = configData.pipeline_start_time || '';\nconst pipeline_start_timestamp = configData.pipeline_start_timestamp || Date.now();\n\nconsole.log('=== Processing File List ===');\nconsole.log('Pipeline start time:', pipeline_start_time);\nconsole.log('Config data:', configData);\nconsole.log('Raw output:', output);\nconsole.log('Files found:', output ? output.split(/\\r?\\n/).filter(x => x.trim()).length : 0);\n\nconst config = {\n  converter_path: configData.converter_path || '',\n  source_folder: configData.source_folder || '',\n  output_folder: configData.output_folder || '',\n  include_subfolders: configData.include_subfolders || false,\n  file_extension: configData.file_extension || '.rvt',\n  options: configData.options || ''\n};\n\nif (!output || !output.trim()) {\n  return [{\n    json: {\n      files: [],\n      total_files: 0,\n      message: `No files found with extension '${config.file_extension}' in ${config.source_folder}`,\n      error: stderr || 'No output from search command',\n      config: config,\n      pipeline_start_time: pipeline_start_time,\n      pipeline_start_timestamp: pipeline_start_timestamp\n    }\n  }];\n}\n\nlet normalizedExtension = (config.file_extension || '').trim();\nif (normalizedExtension && !normalizedExtension.startsWith('.')) {\n  normalizedExtension = '.' + normalizedExtension;\n}\n\nlet rawPaths = [];\ntry {\n  rawPaths = output.trim().split(/\\r?\\n/)\n    .map(path => (path || '').trim())\n    .filter(path => path && path.length > 0);\n} catch (error) {\n  console.log('Error splitting output:', error);\n  return [{\n    json: {\n      files: [],\n      total_files: 0,\n      message: `Error processing search results: ${error.message}`,\n      config: config,\n      pipeline_start_time: pipeline_start_time,\n      pipeline_start_timestamp: pipeline_start_timestamp\n    }\n  }];\n}\n\nconst filePaths = rawPaths.filter(path => {\n  if (!path || !normalizedExtension) return false;\n  const lowerPath = path.toLowerCase();\n  const lowerExt = normalizedExtension.toLowerCase();\n  return lowerPath.endsWith(lowerExt);\n});\n\nif (filePaths.length === 0) {\n  return [{\n    json: {\n      files: [],\n      total_files: 0,\n      message: `No files found with extension '${normalizedExtension}' in ${config.source_folder}`,\n      config: config,\n      pipeline_start_time: pipeline_start_time,\n      pipeline_start_timestamp: pipeline_start_timestamp\n    }\n  }];\n}\n\nconst files = filePaths.map((filePath, index) => {\n  let fileName = 'unknown_file';\n  let fileNameWithoutExt = 'unknown_file';\n  \n  try {\n    if (filePath && typeof filePath === 'string') {\n      const normalizedPath = filePath.replace(/\\\\/g, '/');\n      const pathParts = normalizedPath.split('/');\n      fileName = pathParts[pathParts.length - 1] || 'unknown_file';\n      \n      console.log(`Processing file ${index + 1}: ${fileName} from path: ${filePath}`);\n      \n      const lastDotIndex = fileName.lastIndexOf('.');\n      if (lastDotIndex > 0) {\n        fileNameWithoutExt = fileName.substring(0, lastDotIndex);\n      } else {\n        fileNameWithoutExt = fileName;\n      }\n    }\n  } catch (error) {\n    console.log(`Error parsing file path ${filePath}:`, error);\n  }\n  \n  const extWithoutDot = normalizedExtension ? normalizedExtension.substring(1) : 'unknown';\n  \n  return {\n    index: index + 1,\n    file_path: filePath,\n    file_name: fileName,\n    file_name_without_ext: fileNameWithoutExt,\n    converter_path: config.converter_path,\n    expected_output: `${config.output_folder}\\\\${fileNameWithoutExt}_${extWithoutDot}.xlsx`,\n    expected_output_dae: `${config.output_folder}\\\\${fileNameWithoutExt}_${extWithoutDot}.dae`,\n    config: config,\n    options: config.options,\n    pipeline_start_time: pipeline_start_time,\n    pipeline_start_timestamp: pipeline_start_timestamp\n  };\n});\n\nconsole.log('Processed files:', files.map(f => f.file_name));\n\nreturn [{\n  json: {\n    files: files,\n    total_files: files.length,\n    message: `Found ${files.length} files to convert`,\n    config: config,\n    pipeline_start_time: pipeline_start_time,\n    pipeline_start_timestamp: pipeline_start_timestamp\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "357d21f1-ff0b-4308-99a7-8ab3140b6dde",
      "name": "检查文件是否存在 1",
      "type": "n8n-nodes-base.if",
      "position": [
        1328,
        576
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "files-exist-condition",
              "operator": {
                "type": "number",
                "operation": "gt"
              },
              "leftValue": "={{ $json.files && $json.files.length || 0 }}",
              "rightValue": 0
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "84eff3b1-f5a8-4c84-82cb-2fa1d1bc23ea",
      "name": "拆分文件进行处理 1",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        1520,
        560
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "files"
      },
      "typeVersion": 1
    },
    {
      "id": "0330e04c-9572-439d-b09f-aa932b5e81ad",
      "name": "创建输出目录 1",
      "type": "n8n-nodes-base.executeCommand",
      "position": [
        1696,
        656
      ],
      "parameters": {
        "command": "=mkdir \"{{ $json.config.output_folder }}\" 2>nul || echo Output directory ready",
        "executeOnce": false
      },
      "typeVersion": 1
    },
    {
      "id": "1cbf1ee9-0710-4145-a54e-0c4c73e177a6",
      "name": "合并文件数据 1",
      "type": "n8n-nodes-base.merge",
      "position": [
        1856,
        576
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3
    },
    {
      "id": "627097e5-5d07-4386-a9d7-22dffe7ba4ab",
      "name": "捕获开始时间 1",
      "type": "n8n-nodes-base.code",
      "position": [
        2032,
        576
      ],
      "parameters": {
        "jsCode": "// Capture conversion start time and preserve ALL data including pipeline start\nreturn $input.all().map(item => {\n  const json = {...item.json};\n  json.conversion_start_time = new Date().toISOString();\n  json.conversion_start_timestamp = Date.now();\n  console.log(`Starting conversion for: ${json.file_name} at ${json.conversion_start_time}`);\n  console.log(`Pipeline started at: ${json.pipeline_start_time}`);\n  return {json};\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "edf42bdd-67ed-46d1-a522-cbc93538d894",
      "name": "短暂延迟",
      "type": "n8n-nodes-base.wait",
      "position": [
        2192,
        576
      ],
      "webhookId": "5a1cb608-ec3e-4832-a289-f6a52ef8bc7a",
      "parameters": {
        "unit": "milliseconds"
      },
      "typeVersion": 1
    },
    {
      "id": "1c4e14d2-f9cc-4b4f-85f0-7c0245c32b18",
      "name": "执行转换 1",
      "type": "n8n-nodes-base.executeCommand",
      "position": [
        2352,
        688
      ],
      "parameters": {
        "command": "=\"{{ $json.converter_path }}\" \"{{ $json.file_path }}\" \"{{ $json.expected_output }}\" \"{{ $json.expected_output_dae }}\" {{ $json.options }}",
        "executeOnce": false
      },
      "typeVersion": 1,
      "continueOnFail": true
    },
    {
      "id": "f02f9f74-9b20-4857-92c3-4b0f4103f1b1",
      "name": "计算转换时间 1",
      "type": "n8n-nodes-base.code",
      "position": [
        2512,
        688
      ],
      "parameters": {
        "jsCode": "// Calculate actual conversion time and rename outputs - preserve pipeline start time\nreturn $input.all().map(item => {\n  const json = {...item.json};\n  \n  // Rename stdout/stderr\n  json.conversion_stdout = json.stdout || '';\n  json.conversion_stderr = json.stderr || '';\n  json.conversion_exitCode = json.exitCode || 0;\n  delete json.stdout;\n  delete json.stderr;\n  delete json.exitCode;\n  \n  // Calculate actual processing time with decimal precision\n  if (json.conversion_start_timestamp) {\n    const endTime = Date.now();\n    const startTime = json.conversion_start_timestamp;\n    const processingTimeMs = endTime - startTime;\n    json.processing_time = processingTimeMs / 1000;\n    json.processing_time_ms = processingTimeMs;\n    json.conversion_end_time = new Date().toISOString();\n    json.conversion_end_timestamp = endTime;\n    console.log(`Conversion completed for: ${json.file_name} in ${json.processing_time.toFixed(2)} seconds`);\n  } else {\n    json.processing_time = 0;\n    json.processing_time_ms = 0;\n    console.warn(`No start timestamp found for: ${json.file_name}`);\n  }\n  \n  // Ensure pipeline start time is preserved\n  console.log(`Pipeline start preserved: ${json.pipeline_start_timestamp}`);\n  \n  return {json};\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "9255e34b-be94-4630-8887-fc7a0aafd739",
      "name": "验证前合并数据 1",
      "type": "n8n-nodes-base.merge",
      "position": [
        2672,
        576
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3
    },
    {
      "id": "0e3dc51e-4891-4094-ad20-341424dd04df",
      "name": "验证输出文件并获取大小 1",
      "type": "n8n-nodes-base.executeCommand",
      "position": [
        2832,
        576
      ],
      "parameters": {
        "command": "=powershell -Command \"$xlsx='{{ $json.expected_output }}'; $dae='{{ $json.expected_output_dae }}'; $revit='{{ $json.file_path }}'; $xlsxExists=Test-Path -LiteralPath $xlsx; $daeExists=Test-Path -LiteralPath $dae; $revitExists=Test-Path -LiteralPath $revit; $xlsxSize=if($xlsxExists){(Get-Item -LiteralPath $xlsx).Length}else{0}; $daeSize=if($daeExists){(Get-Item -LiteralPath $dae).Length}else{0}; $revitSize=if($revitExists){(Get-Item -LiteralPath $revit).Length}else{0}; Write-Output ('XLSX_EXISTS:'+$xlsxExists+'|XLSX_SIZE:'+$xlsxSize+'|DAE_EXISTS:'+$daeExists+'|DAE_SIZE:'+$daeSize+'|REVIT_EXISTS:'+$revitExists+'|REVIT_SIZE:'+$revitSize)\"",
        "executeOnce": false
      },
      "typeVersion": 1
    },
    {
      "id": "8d038788-44e7-49be-a999-b97ac4f96fac",
      "name": "完成文件验证 1",
      "type": "n8n-nodes-base.code",
      "position": [
        2992,
        576
      ],
      "parameters": {
        "jsCode": "// Complete file verification with processing time - preserve pipeline start time\nconst allInputs = $input.all();\nconst mergedDataItems = $('Merge Data Before Verification1').all();\n\nconsole.log(`=== Complete File Verification ===`);\nconsole.log(`Total verification inputs: ${allInputs.length}`);\nconsole.log(`Total merged data items: ${mergedDataItems.length}`);\n\nconst results = [];\n\n// Process each file\nallInputs.forEach((input, index) => {\n  const verificationData = input.json || {};\n  \n  // Find corresponding merged data by matching index\n  const mergedData = mergedDataItems[index]?.json || {};\n  \n  // Extract all necessary data from merged node\n  const fileName = mergedData.file_name || 'unknown_file';\n  const filePath = mergedData.file_path || '';\n  const expectedOutput = mergedData.expected_output || '';\n  const expectedOutputDae = mergedData.expected_output_dae || '';\n  const fileIndex = mergedData.index || index + 1;\n  const processingTime = mergedData.processing_time || 0;\n  const config = mergedData.config || {};\n  const conversionStartTime = mergedData.conversion_start_time || '';\n  const conversionEndTime = mergedData.conversion_end_time || '';\n  const conversionEndTimestamp = mergedData.conversion_end_timestamp || Date.now();\n  \n  // IMPORTANT: Preserve pipeline start time\n  const pipelineStartTime = mergedData.pipeline_start_time || '';\n  const pipelineStartTimestamp = mergedData.pipeline_start_timestamp || 0;\n\n  console.log(`Processing verification for file ${fileIndex}: ${fileName}`);\n  console.log('Actual processing time:', processingTime, 'seconds');\n  console.log('Pipeline start timestamp:', pipelineStartTimestamp);\n\n  // Get verification output from the current input\n  let verificationOutput = verificationData.stdout || verificationData.verification_output || '';\n\n  // Parse PowerShell output\n  let successXlsx = false;\n  let successDae = false;\n  let successRevit = false;\n  let fileSizeXlsx = 0;\n  let fileSizeDae = 0;\n  let fileSizeRevit = 0;\n\n  try {\n    if (verificationOutput && verificationOutput.includes('|')) {\n      const parts = verificationOutput.split('|');\n      \n      parts.forEach(part => {\n        if (part && part.includes('XLSX_EXISTS:')) {\n          successXlsx = part.includes('XLSX_EXISTS:True');\n        }\n        if (part && part.includes('XLSX_SIZE:')) {\n          const sizeStr = part.replace('XLSX_SIZE:', '').trim();\n          fileSizeXlsx = parseInt(sizeStr) || 0;\n        }\n        if (part && part.includes('DAE_EXISTS:')) {\n          successDae = part.includes('DAE_EXISTS:True');\n        }\n        if (part && part.includes('DAE_SIZE:')) {\n          const sizeStr = part.replace('DAE_SIZE:', '').trim();\n          fileSizeDae = parseInt(sizeStr) || 0;\n        }\n        if (part && part.includes('REVIT_EXISTS:')) {\n          successRevit = part.includes('REVIT_EXISTS:True');\n        }\n        if (part && part.includes('REVIT_SIZE:')) {\n          const sizeStr = part.replace('REVIT_SIZE:', '').trim();\n          fileSizeRevit = parseInt(sizeStr) || 0;\n        }\n      });\n    }\n  } catch (error) {\n    console.log('Error parsing verification output:', error);\n  }\n\n  const finalSuccess = successXlsx && successDae && fileSizeXlsx > 0 && fileSizeDae > 0;\n\n  const statusMessage = finalSuccess \n    ? `✅ [${fileIndex}] Successfully converted: ${fileName} (XLSX: ${Math.round(fileSizeXlsx/1024)}KB, DAE: ${Math.round(fileSizeDae/1024)}KB, ${processingTime.toFixed(1)}s)`\n    : `❌ [${fileIndex}] Failed to convert: ${fileName} (XLSX: ${successXlsx && fileSizeXlsx > 0 ? '✓' : '✗'}, DAE: ${successDae && fileSizeDae > 0 ? '✓' : '✗'}, ${processingTime.toFixed(1)}s)`;\n\n  const result = {\n    file_name: fileName,\n    file_path: filePath,\n    expected_output: expectedOutput,\n    expected_output_dae: expectedOutputDae,\n    success: finalSuccess,\n    success_xlsx: successXlsx && fileSizeXlsx > 0,\n    success_dae: successDae && fileSizeDae > 0,\n    processing_time: processingTime,\n    file_size: fileSizeXlsx + fileSizeDae,\n    file_size_xlsx: fileSizeXlsx,\n    file_size_dae: fileSizeDae,\n    file_size_revit: fileSizeRevit,\n    index: fileIndex,\n    status: finalSuccess ? 'converted' : 'failed',\n    message: statusMessage,\n    timestamp: new Date().toISOString(),\n    verification_output: verificationOutput,\n    config: config,\n    conversion_start_time: conversionStartTime,\n    conversion_end_time: conversionEndTime,\n    conversion_end_timestamp: conversionEndTimestamp,\n    pipeline_start_time: pipelineStartTime,\n    pipeline_start_timestamp: pipelineStartTimestamp\n  };\n\n  console.log(`Verification complete for ${fileName}: ${result.message}`);\n  results.push({ json: result });\n});\n\nconsole.log(`=== Returning ${results.length} verification results ===`);\nreturn results;"
      },
      "typeVersion": 2
    },
    {
      "id": "44336609-1655-4f5b-92b4-9b6165620036",
      "name": "生成 HTML 报告 1",
      "type": "n8n-nodes-base.code",
      "position": [
        3152,
        576
      ],
      "parameters": {
        "jsCode": "// Generate HTML Report with CORRECT pipeline timing and no-collada handling\nconst results = $input.all();\n\n// Get the current time as pipeline end\nconst pipelineEndTime = Date.now();\n\n// Find the pipeline start timestamp from the data\nlet pipelineStartTimestamp = null;\nlet config = {};\n\n// Extract timing and config from results\nfor (const result of results) {\n  if (result.json) {\n    // Get pipeline start timestamp\n    if (result.json.pipeline_start_timestamp && !pipelineStartTimestamp) {\n      pipelineStartTimestamp = result.json.pipeline_start_timestamp;\n      console.log('Found pipeline start timestamp:', pipelineStartTimestamp);\n    }\n    \n    // Get config\n    if (result.json.config && Object.keys(result.json.config).length > 0) {\n      config = result.json.config;\n    }\n  }\n}\n\n// Check if no-collada option is enabled\nconst noColladaEnabled = config.options && config.options.includes('no-collada');\nconsole.log('No-collada mode enabled:', noColladaEnabled);\n\n// Calculate total pipeline time in minutes\nlet totalPipelineMinutes = '0.00';\nif (pipelineStartTimestamp) {\n  const totalMs = pipelineEndTime - pipelineStartTimestamp;\n  totalPipelineMinutes = (totalMs / 60000).toFixed(2);\n  console.log('Pipeline timing:', {\n    start: pipelineStartTimestamp,\n    end: pipelineEndTime,\n    totalMs: totalMs,\n    totalMinutes: totalPipelineMinutes\n  });\n} else {\n  console.warn('No pipeline start timestamp found!');\n}\n\n// Use config output folder or fallback\nconst outputFolder = config.output_folder || 'C:\\\\temp';\n\n// Count successful and failed conversions\nlet totalFiles = 0;\nlet successfulConversions = 0;\nlet failedConversions = 0;\nlet totalInputSize = 0;\nlet totalOutputSize = 0;\n\nconst successfulFiles = [];\nconst failedFiles = [];\n\nresults.forEach((result, index) => {\n  const data = result.json || {};\n  totalFiles++;\n  \n  const processingTime = data.processing_time || 0;\n  const xlsxSuccess = data.success_xlsx || false;\n  const daeSuccess = data.success_dae || false;\n  const fileSizeRevit = data.file_size_revit || 0;\n  const fileSizeXlsx = data.file_size_xlsx || 0;\n  const fileSizeDae = data.file_size_dae || 0;\n  \n  totalInputSize += fileSizeRevit;\n  \n  // Modified success logic for no-collada mode\n  let isSuccess = false;\n  if (noColladaEnabled) {\n    // In no-collada mode, only XLSX matters\n    isSuccess = xlsxSuccess;\n    if (isSuccess) {\n      totalOutputSize += fileSizeXlsx;\n    }\n  } else {\n    // Normal mode: both XLSX and DAE must succeed\n    isSuccess = xlsxSuccess && daeSuccess;\n    if (isSuccess) {\n      totalOutputSize += fileSizeXlsx + fileSizeDae;\n    }\n  }\n  \n  if (isSuccess) {\n    successfulConversions++;\n    successfulFiles.push({\n      name: data.file_name || `File ${index + 1}`,\n      originalSize: Math.round(fileSizeRevit / 1024),\n      xlsxSize: Math.round(fileSizeXlsx / 1024),\n      daeSize: Math.round(fileSizeDae / 1024),\n      totalSize: Math.round((fileSizeXlsx + (noColladaEnabled ? 0 : fileSizeDae)) / 1024),\n      xlsxPath: data.expected_output || '',\n      daePath: data.expected_output_dae || '',\n      noCollada: noColladaEnabled\n    });\n  } else {\n    failedConversions++;\n    failedFiles.push({\n      name: data.file_name || `File ${index + 1}`,\n      originalSize: Math.round(fileSizeRevit / 1024),\n      xlsxStatus: xlsxSuccess ? '✓' : '✗',\n      daeStatus: daeSuccess ? '✓' : '✗'\n    });\n  }\n});\n\nconst successRate = totalFiles > 0 ? Math.round((successfulConversions / totalFiles) * 100) : 0;\n\n// Generate timestamp for filename\nconst now = new Date();\nconst timestamp = now.toISOString().slice(0,19).replace(/:/g, '-');\nconst fileName = `CAD_Conversion_Report_${timestamp}.html`;\nconst fullPath = `${outputFolder}\\\\${fileName}`.replace(/\\\\\\\\/g, '\\\\');\n\nconsole.log('Report summary:', {\n  totalFiles,\n  successfulConversions,\n  failedConversions,\n  successRate: successRate + '%',\n  totalPipelineTime: totalPipelineMinutes + ' minutes',\n  outputPath: fullPath,\n  noColladaMode: noColladaEnabled\n});\n\n// Create HTML content with professional styling\nconst htmlContent = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>CAD Project Batch Conversion Report</title>\n    <style>\n        * {\n            margin: 0;\n            padding: 0;\n            box-sizing: border-box;\n        }\n        \n        body { \n            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; \n            background: #f5f6fa;\n            color: #2c3e50;\n            line-height: 1.6;\n        }\n        \n        .container { \n            max-width: 1400px; \n            margin: 0 auto; \n            background: white; \n            box-shadow: 0 0 40px rgba(0, 0, 0, 0.08); \n        }\n        \n        .header { \n            background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);\n            color: white; \n            padding: 60px 40px;\n            position: relative;\n            overflow: hidden;\n        }\n        \n        .header::before {\n            content: '';\n            position: absolute;\n            top: -50%;\n            right: -10%;\n            width: 40%;\n            height: 200%;\n            background: rgba(255, 255, 255, 0.05);\n            transform: rotate(35deg);\n        }\n        \n        .header-content {\n            position: relative;\n            z-index: 1;\n        }\n        \n        .header h1 { \n            font-size: 2.8rem; \n            font-weight: 300;\n            letter-spacing: -1px;\n            margin-bottom: 15px;\n        }\n        \n        .header .subtitle { \n            font-size: 1.1rem;\n            opacity: 0.85;\n            font-weight: 400;\n        }\n        \n        .header .timestamp {\n            margin-top: 20px;\n            font-size: 0.95rem;\n            opacity: 0.7;\n        }\n        \n        .content {\n            padding: 40px;\n        }\n        \n        .metrics { \n            display: grid; \n            grid-template-columns: repeat(5, 1fr); \n            gap: 20px; \n            margin: -20px 0 40px 0;\n            position: relative;\n            z-index: 10;\n        }\n        \n        @media (max-width: 1200px) {\n            .metrics {\n                grid-template-columns: repeat(3, 1fr);\n            }\n        }\n        \n        @media (max-width: 768px) {\n            .metrics {\n                grid-template-columns: repeat(2, 1fr);\n            }\n        }\n        \n        @media (max-width: 480px) {\n            .metrics {\n                grid-template-columns: 1fr;\n            }\n        }\n        \n        .metric { \n            background: white;\n            padding: 30px 20px;\n            border-radius: 12px;\n            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\n            transition: all 0.3s ease;\n            border: 1px solid #e8ecf1;\n            position: relative;\n            overflow: hidden;\n            text-align: center;\n        }\n        \n        .metric::before {\n            content: '';\n            position: absolute;\n            top: 0;\n            left: 0;\n            width: 100%;\n            height: 4px;\n            background: linear-gradient(90deg, #3b82f6 0%, #2563eb 100%);\n        }\n        \n        .metric:hover {\n            transform: translateY(-3px);\n            box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\n        }\n        \n        .metric-value { \n            font-size: 2.2rem; \n            font-weight: 600;\n            color: #1e3c72;\n            margin-bottom: 8px;\n            line-height: 1;\n        }\n        \n        .metric-label { \n            font-size: 0.85rem; \n            color: #64748b;\n            font-weight: 500;\n            text-transform: uppercase;\n            letter-spacing: 0.5px;\n        }\n        \n        .metric.highlight {\n            background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);\n            border-color: #3b82f6;\n        }\n        \n        .metric.highlight::before {\n            background: linear-gradient(90deg, #10b981 0%, #059669 100%);\n        }\n        \n        .section { \n            margin: 40px 0;\n            background: white;\n            border-radius: 12px;\n            overflow: hidden;\n            box-shadow: 0 2px 15px rgba(0, 0, 0, 0.06);\n            border: 1px solid #e8ecf1;\n        }\n        \n        .section-header {\n            background: #f8fafc;\n            padding: 24px 32px;\n            border-bottom: 1px solid #e8ecf1;\n        }\n        \n        .section h2 { \n            color: #1e293b;\n            font-size: 1.4rem;\n            font-weight: 600;\n            display: flex;\n            align-items: center;\n            gap: 10px;\n        }\n        \n        .section-content {\n            padding: 0;\n        }\n        \n        table { \n            width: 100%; \n            border-collapse: collapse;\n        }\n        \n        th, td { \n            padding: 16px 24px;\n            text-align: left;\n            border-bottom: 1px solid #f1f5f9;\n        }\n        \n        th { \n            background: #f8fafc;\n            font-weight: 600;\n            color: #475569;\n            font-size: 0.875rem;\n            text-transform: uppercase;\n            letter-spacing: 0.5px;\n            position: sticky;\n            top: 0;\n            z-index: 1;\n        }\n        \n        tr:last-child td {\n            border-bottom: none;\n        }\n        \n        tr:hover { \n            background: #f8fafc;\n        }\n        \n        .status-success { \n            background: #10b981;\n            color: white;\n            padding: 6px 12px;\n            border-radius: 6px;\n            font-size: 0.75rem;\n            font-weight: 600;\n            display: inline-block;\n            text-transform: uppercase;\n            letter-spacing: 0.5px;\n        }\n        \n        .status-failed { \n            background: #ef4444;\n            color: white;\n            padding: 6px 12px;\n            border-radius: 6px;\n            font-size: 0.75rem;\n            font-weight: 600;\n            display: inline-block;\n            text-transform: uppercase;\n            letter-spacing: 0.5px;\n        }\n        \n        .status-skipped { \n            background: #6b7280;\n            color: white;\n            padding: 6px 12px;\n            border-radius: 6px;\n            font-size: 0.75rem;\n            font-weight: 600;\n            display: inline-block;\n            text-transform: uppercase;\n            letter-spacing: 0.5px;\n        }\n        \n        .config-section {\n            background: #f8fafc;\n            padding: 32px;\n            border-radius: 12px;\n            margin: 40px 0;\n        }\n        \n        .config-grid {\n            display: grid;\n            grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));\n            gap: 24px;\n            margin-top: 20px;\n        }\n        \n        .config-item {\n            display: flex;\n            align-items: flex-start;\n            gap: 16px;\n        }\n        \n        .config-label {\n            font-weight: 600;\n            color: #475569;\n            min-width: 140px;\n            font-size: 0.9rem;\n        }\n        \n        .config-value {\n            color: #1e293b;\n            font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;\n            background: white;\n            padding: 8px 12px;\n            border-radius: 6px;\n            font-size: 0.85rem;\n            word-break: break-all;\n            flex: 1;\n            border: 1px solid #e2e8f0;\n        }\n        \n        .footer { \n            background: #f8fafc;\n            padding: 40px;\n            text-align: center;\n            color: #64748b;\n            border-top: 1px solid #e8ecf1;\n        }\n        \n        .footer .company {\n            font-size: 1.1rem;\n            font-weight: 600;\n            color: #1e293b;\n            margin-bottom: 8px;\n        }\n        \n        .footer .version {\n            font-size: 0.85rem;\n            margin-top: 16px;\n            color: #94a3b8;\n        }\n        \n        .report-location {\n            background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);\n            border: 1px solid #3b82f6;\n            border-radius: 12px;\n            padding: 20px 28px;\n            margin: 30px 0;\n            text-align: center;\n            color: #1e3c72;\n            font-size: 0.95rem;\n        }\n        \n        .report-location strong {\n            color: #1e3c72;\n            font-weight: 600;\n        }\n        \n        .size-cell {\n            font-family: 'SF Mono', Monaco, monospace;\n            font-size: 0.9rem;\n        }\n        \n        .file-link {\n            color: #3b82f6;\n            text-decoration: none;\n            font-weight: 500;\n            transition: all 0.2s ease;\n            position: relative;\n            display: inline-block;\n            padding: 2px 0;\n        }\n        \n        .file-link:hover {\n            color: #2563eb;\n            text-decoration: underline;\n        }\n        \n        .tooltip {\n            position: relative;\n            display: inline-block;\n        }\n        \n        .tooltip .tooltiptext {\n            visibility: hidden;\n            width: 280px;\n            background-color: #333;\n            color: #fff;\n            text-align: center;\n            border-radius: 8px;\n            padding: 12px 16px;\n            position: absolute;\n            z-index: 1000;\n            bottom: 125%;\n            left: 50%;\n            margin-left: -140px;\n            opacity: 0;\n            transition: opacity 0.3s;\n            font-size: 0.85rem;\n            line-height: 1.4;\n            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);\n        }\n        \n        .tooltip .tooltiptext::after {\n            content: \"\";\n            position: absolute;\n            top: 100%;\n            left: 50%;\n            margin-left: -5px;\n            border-width: 5px;\n            border-style: solid;\n            border-color: #333 transparent transparent transparent;\n        }\n        \n        .tooltip:hover .tooltiptext {\n            visibility: visible;\n            opacity: 1;\n        }\n        \n        .no-collada-notice {\n            background: #fef3c7;\n            border: 1px solid #fbbf24;\n            border-radius: 8px;\n            padding: 16px;\n            margin: 20px 0;\n            color: #92400e;\n            font-size: 0.9rem;\n            text-align: center;\n        }\n        \n        .no-collada-notice strong {\n            color: #78350f;\n        }\n        \n        @media print {\n            body { background: white; }\n            .container { box-shadow: none; }\n            .metric { box-shadow: none; border: 1px solid #e5e7eb; }\n            .section { box-shadow: none; page-break-inside: avoid; }\n            .tooltip .tooltiptext { display: none; }\n        }\n    </style>\n</head>\n<body>\n    <div class=\"container\">\n        <div class=\"header\">\n            <div class=\"header-content\">\n                <h1>CAD Project Batch Conversion Report</h1>\n                <p class=\"subtitle\">Automated conversion pipeline execution summary</p>\n                <p class=\"timestamp\">${now.toLocaleString('en-US', { \n                    weekday: 'long', \n                    year: 'numeric', \n                    month: 'long', \n                    day: 'numeric', \n                    hour: '2-digit', \n                    minute: '2-digit' \n                })}</p>\n            </div>\n        </div>\n        \n        <div class=\"content\">\n            <div class=\"metrics\">\n                <div class=\"metric\">\n                    <div class=\"metric-value\">${totalFiles}</div>\n                    <div class=\"metric-label\">Total Files</div>\n                </div>\n                <div class=\"metric highlight\">\n                    <div class=\"metric-value\">${successRate}%</div>\n                    <div class=\"metric-label\">Success Rate</div>\n                </div>\n                <div class=\"metric\">\n                    <div class=\"metric-value\">${totalPipelineMinutes}</div>\n                    <div class=\"metric-label\">Total Time (Minutes)</div>\n                </div>\n                <div class=\"metric\">\n                    <div class=\"metric-value\">${Math.round(totalInputSize / 1048576)} MB</div>\n                    <div class=\"metric-label\">Total Input Size</div>\n                </div>\n                <div class=\"metric\">\n                    <div class=\"metric-value\">${Math.round(totalOutputSize / 1048576)} MB</div>\n                    <div class=\"metric-label\">Total Output Size</div>\n                </div>\n            </div>\n\n            <div class=\"report-location\">\n                <strong>Report Location:</strong> ${fullPath}\n            </div>\n\n            ${noColladaEnabled ? `\n            <div class=\"no-collada-notice\">\n                <strong>Note:</strong> DAE (Collada) file generation was skipped as per configuration (no-collada option enabled).\n            </div>\n            ` : ''}\n\n            ${successfulFiles.length > 0 ? `\n            <div class=\"section\">\n                <div class=\"section-header\">\n                    <h2>✅ Successfully Converted Files (${successfulFiles.length})</h2>\n                </div>\n                <div class=\"section-content\">\n                    <table>\n                        <thead>\n                            <tr>\n                                <th>File Name</th>\n                                <th>Original Size</th>\n                                <th>Excel Output</th>\n                                ${noColladaEnabled ? '<th>DAE Output</th>' : '<th>DAE Output</th>'}\n                                <th>Total Output</th>\n                            </tr>\n                        </thead>\n                        <tbody>\n                            ${successfulFiles.map(file => `\n                                <tr>\n                                    <td><strong>${file.name}</strong></td>\n                                    <td class=\"size-cell\">${file.originalSize} KB</td>\n                                    <td class=\"size-cell\">\n                                        <a href=\"file:///${file.xlsxPath.replace(/\\\\/g, '/')}\" class=\"file-link\" target=\"_blank\">${file.xlsxSize} KB</a>\n                                    </td>\n                                    <td class=\"size-cell\">\n                                        ${noColladaEnabled ? \n                                            '<span class=\"status-skipped\">SKIPPED</span>' : \n                                            `<div class=\"tooltip\">\n                                                <a href=\"file:///${file.daePath.replace(/\\\\/g, '/')}\" class=\"file-link\" target=\"_blank\">${file.daeSize} KB</a>\n                                                <span class=\"tooltiptext\">💡 Tip: Use the free CAD Assistant software for the best viewing experience of DAE files</span>\n                                            </div>`\n                                        }\n                                    </td>\n                                    <td class=\"size-cell\"><strong>${file.totalSize} KB</strong></td>\n                                </tr>\n                            `).join('')}\n                        </tbody>\n                    </table>\n                </div>\n            </div>\n            ` : ''}\n\n            ${failedFiles.length > 0 ? `\n            <div class=\"section\">\n                <div class=\"section-header\">\n                    <h2>❌ Failed Conversions (${failedFiles.length})</h2>\n                </div>\n                <div class=\"section-content\">\n                    <table>\n                        <thead>\n                            <tr>\n                                <th>File Name</th>\n                                <th>Original Size</th>\n                                <th>XLSX Status</th>\n                                <th>DAE Status</th>\n                            </tr>\n                        </thead>\n                        <tbody>\n                            ${failedFiles.map(file => `\n                                <tr>\n                                    <td><strong>${file.name}</strong></td>\n                                    <td class=\"size-cell\">${file.originalSize} KB</td>\n                                    <td><span class=\"status-${file.xlsxStatus === '✓' ? 'success' : 'failed'}\">${file.xlsxStatus === '✓' ? 'SUCCESS' : 'FAILED'}</span></td>\n                                    <td>${noColladaEnabled && file.xlsxStatus === '✓' ? \n                                        '<span class=\"status-skipped\">SKIPPED</span>' : \n                                        `<span class=\"status-${file.daeStatus === '✓' ? 'success' : 'failed'}\">${file.daeStatus === '✓' ? 'SUCCESS' : 'FAILED'}</span>`\n                                    }</td>\n                                </tr>\n                            `).join('')}\n                        </tbody>\n                    </table>\n                </div>\n            </div>\n            ` : ''}\n\n            <div class=\"config-section\">\n                <h2 style=\"color: #1e293b; font-size: 1.4rem; font-weight: 600; margin-bottom: 20px;\">\n                    ⚙️ Configuration Details\n                </h2>\n                <div class=\"config-grid\">\n                    <div class=\"config-item\">\n                        <span class=\"config-label\">Source Folder:</span>\n                        <span class=\"config-value\">${config.source_folder || 'Not specified'}</span>\n                    </div>\n                    <div class=\"config-item\">\n                        <span class=\"config-label\">Output Folder:</span>\n                        <span class=\"config-value\">${config.output_folder || 'Not specified'}</span>\n                    </div>\n                    <div class=\"config-item\">\n                        <span class=\"config-label\">File Extension:</span>\n                        <span class=\"config-value\">${config.file_extension || 'Not specified'}</span>\n                    </div>\n                    <div class=\"config-item\">\n                        <span class=\"config-label\">Include Subfolders:</span>\n                        <span class=\"config-value\">${config.include_subfolders ? 'Yes' : 'No'}</span>\n                    </div>\n                    <div class=\"config-item\">\n                        <span class=\"config-label\">Converter Options:</span>\n                        <span class=\"config-value\">${config.options || 'None'}</span>\n                    </div>\n                    <div class=\"config-item\">\n                        <span class=\"config-label\">Generated:</span>\n                        <span class=\"config-value\">${now.toLocaleString()}</span>\n                    </div>\n                    <div class=\"config-item\">\n                        <span class=\"config-label\">Workflow ID:</span>\n                        <span class=\"config-value\">FYFQhblt4gILLSpe</span>\n                    </div>\n                </div>\n            </div>\n        </div>\n\n        <div class=\"footer\">\n            <div class=\"company\">DataDrivenConstruction.io</div>\n            <div>Professional CAD Conversion Pipeline</div>\n            <div class=\"version\">Version 2.0 | Automated Batch Processing System</div>\n        </div>\n    </div>\n</body>\n</html>`;\n\n// Return data for saving and opening\nreturn [{\n    json: {\n        html_content: htmlContent,\n        file_name: fileName,\n        full_path: fullPath,\n        output_folder: outputFolder,\n        total_files: totalFiles,\n        successful_conversions: successfulConversions,\n        failed_conversions: failedConversions,\n        success_rate: successRate,\n        total_pipeline_time: totalPipelineMinutes,\n        config: config,\n        summary_message: `✅ Conversion Complete! ${successfulConversions}/${totalFiles} files converted successfully (${successRate}% success rate) in ${totalPipelineMinutes} minutes`\n    }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "21c9bd6d-e1a0-4697-b99d-c51e666af986",
      "name": "准备二进制数据 1",
      "type": "n8n-nodes-base.code",
      "position": [
        3312,
        576
      ],
      "parameters": {
        "jsCode": "// Prepare binary data for file saving\nconst item = $input.first().json;\n\nconsole.log('Preparing binary data for file:', item.file_name);\nconsole.log('Full path:', item.full_path);\n\nif (!item.html_content) {\n  throw new Error('No HTML content found');\n}\n\nreturn [{\n  json: item,\n  binary: {\n    report: {\n      data: Buffer.from(item.html_content, 'utf-8').toString('base64'),\n      mimeType: 'text/html',\n      fileName: item.file_name,\n      fileExtension: 'html'\n    }\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "3ba8b465-5da4-4b79-83e2-2bdb63222b6b",
      "name": "保存 HTML 文件 1",
      "type": "n8n-nodes-base.writeBinaryFile",
      "position": [
        3472,
        576
      ],
      "parameters": {
        "options": {},
        "fileName": "={{ $json.full_path }}",
        "dataPropertyName": "report"
      },
      "typeVersion": 1
    },
    {
      "id": "94de0bbb-402b-4dea-bcaf-f98fdc678048",
      "name": "验证并准备路径 1",
      "type": "n8n-nodes-base.code",
      "position": [
        3632,
        576
      ],
      "parameters": {
        "jsCode": "// Verify file was saved and prepare for opening\nconst data = $input.first().json;\nconst fullPath = data.full_path || '';\n\nconsole.log('Preparing to open file:', fullPath);\n\n// Normalize path for Windows\nconst windowsPath = fullPath.replace(/\\\\\\\\/g, '\\\\').replace(/\\//g, '\\\\');\n\nreturn [{\n  json: {\n    ...data,\n    windows_path: windowsPath,\n    command_to_open: `cmd /c start \"\" \"${windowsPath}\"`\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "6558ac19-23c1-44c9-8b7b-3615052cd4a5",
      "name": "打开 HTML 报告 1",
      "type": "n8n-nodes-base.executeCommand",
      "position": [
        3792,
        576
      ],
      "parameters": {
        "command": "=cmd /c start \"\" \"{{ $json.windows_path }}\""
      },
      "typeVersion": 1
    },
    {
      "id": "cdf12580-ade1-4ae0-a699-980aca156c98",
      "name": "最终完成通知 1",
      "type": "n8n-nodes-base.executeCommand",
      "position": [
        3952,
        576
      ],
      "parameters": {
        "command": "=echo 🏁 CAD Conversion Pipeline Completed Successfully! & echo 📋 Summary: {{ $json.summary_message }} & echo 📁 Report Location: {{ $json.windows_path }} & echo ✨ All processes finished successfully! & echo. & echo 🎯 Next Steps: & echo    1. Check the HTML report that opened in your browser & echo    2. Verify output files in the specified folder & echo    3. Review conversion results and metrics & echo. & echo ✅ Pipeline execution completed!",
        "executeOnce": false
      },
      "typeVersion": 1
    },
    {
      "id": "dc253ac4-c296-4a8c-897c-380e5e14b683",
      "name": "未找到文件响应 1",
      "type": "n8n-nodes-base.set",
      "position": [
        1520,
        784
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "no-files-message",
              "name": "message",
              "type": "string",
              "value": "⚠️ No files found to convert with the specified parameters"
            },
            {
              "id": "no-files-details",
              "name": "details",
              "type": "string",
              "value": "Please check: 1) Source folder path exists, 2) File extension is correct, 3) Files exist in the specified location"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "ee8bb887-c553-412b-9be5-c865f08fbd82",
      "name": "便签7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -144,
        112
      ],
      "parameters": {
        "color": 6,
        "width": 756,
        "height": 844,
        "content": "## 🏁 组 1:初始化"
      },
      "typeVersion": 1
    },
    {
      "id": "0ca6f619-b073-41b1-a248-0f2b62333773",
      "name": "便签9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        624,
        112
      ],
      "parameters": {
        "color": 5,
        "width": 1024,
        "height": 840,
        "content": "## 🔍 组 2:文件发现"
      },
      "typeVersion": 1
    },
    {
      "id": "8dcff587-4f09-4e86-a065-bdd056be456d",
      "name": "便签10",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1664,
        112
      ],
      "parameters": {
        "color": 5,
        "width": 316,
        "height": 844,
        "content": "## 🔄 组 3:批次准备"
      },
      "typeVersion": 1
    },
    {
      "id": "ae685ee4-bbee-4b56-85e0-26f3da9053fd",
      "name": "便签11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2000,
        112
      ],
      "parameters": {
        "color": 5,
        "width": 800,
        "height": 840,
        "content": "## ⚡ 组 4:转换执行"
      },
      "typeVersion": 1
    },
    {
      "id": "5fe7b6d6-7a36-41ae-a431-a93803227a50",
      "name": "便签15",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2816,
        112
      ],
      "parameters": {
        "color": 6,
        "width": 284,
        "height": 836,
        "content": "## ✅ 组 5:验证"
      },
      "typeVersion": 1
    },
    {
      "id": "2a199a8f-7206-4b18-9e87-f9dc4db56d8d",
      "name": "便签16",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3120,
        112
      ],
      "parameters": {
        "width": 456,
        "height": 832,
        "content": "## 📊 组 6:报告"
      },
      "typeVersion": 1
    },
    {
      "id": "565d9855-8447-4c4e-ab57-b652a8733040",
      "name": "便签17",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3600,
        112
      ],
      "parameters": {
        "width": 484,
        "height": 828,
        "content": "## 🎯 组 7:最终化"
      },
      "typeVersion": 1
    },
    {
      "id": "32324229-f786-4023-bfc1-bc01ad05575d",
      "name": "便签 30",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -144,
        976
      ],
      "parameters": {
        "color": 4,
        "width": 488,
        "height": 308,
        "content": "## 📝 配置示例"
      },
      "typeVersion": 1
    },
    {
      "id": "62e0570c-238d-40e2-8b1f-f1f71ca8f034",
      "name": "计划触发器1",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -80,
        560
      ],
      "parameters": {
        "rule": {
          "interval": [
            {}
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "637d319f-4c7d-4f14-8676-16e6a839812d",
      "name": "便签2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        368,
        304
      ],
      "parameters": {
        "color": 4,
        "width": 192,
        "height": 384,
        "content": "## ⬇️ 仅在此处修改变量"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "7eb4a013-6adf-4f71-b5d8-1c78163410dd",
  "connections": {
    "Small Delay": {
      "main": [
        [
          {
            "node": "Execute Conversion1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Capture Pipeline Start Time1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find CAD Files1": {
      "main": [
        [
          {
            "node": "Merge Config with Search Results1",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Save HTML File1": {
      "main": [
        [
          {
            "node": "Verify and Prepare Path1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge File Data1": {
      "main": [
        [
          {
            "node": "Capture Start Time1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Open HTML Report1": {
      "main": [
        [
          {
            "node": "Final Completion Notice1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process File List1": {
      "main": [
        [
          {
            "node": "Check if Files Exist1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Capture Start Time1": {
      "main": [
        [
          {
            "node": "Small Delay",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge Data Before Verification1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute Conversion1": {
      "main": [
        [
          {
            "node": "Calculate Conversion Time1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Binary Data1": {
      "main": [
        [
          {
            "node": "Save HTML File1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check if Files Exist1": {
      "main": [
        [
          {
            "node": "Split Files for Processing1",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No Files Found Response1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate HTML Report1": {
      "main": [
        [
          {
            "node": "Prepare Binary Data1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Output Directory1": {
      "main": [
        [
          {
            "node": "Merge File Data1",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Verify and Prepare Path1": {
      "main": [
        [
          {
            "node": "Open HTML Report1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Conversion Time1": {
      "main": [
        [
          {
            "node": "Merge Data Before Verification1",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Complete File Verification1": {
      "main": [
        [
          {
            "node": "Generate HTML Report1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Files for Processing1": {
      "main": [
        [
          {
            "node": "Create Output Directory1",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge File Data1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Capture Pipeline Start Time1": {
      "main": [
        [
          {
            "node": "Set Configuration Parameters1",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge Pipeline Start with Config1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Configuration Parameters1": {
      "main": [
        [
          {
            "node": "Merge Pipeline Start with Config1",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge Data Before Verification1": {
      "main": [
        [
          {
            "node": "Verify Output Files and Get Sizes1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Config with Search Results1": {
      "main": [
        [
          {
            "node": "Process File List1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Pipeline Start with Config1": {
      "main": [
        [
          {
            "node": "Find CAD Files1",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge Config with Search Results1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Verify Output Files and Get Sizes1": {
      "main": [
        [
          {
            "node": "Complete File Verification1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
常见问题

如何使用这个工作流?

复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。

这个工作流适合什么场景?

高级 - 文档提取, 多模态 AI

需要付费吗?

本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。

工作流信息
难度等级
高级
节点数量82
分类2
节点类型17
难度说明

适合高级用户,包含 16+ 个节点的复杂工作流

作者

Founder DataDrivenConstruction.io | AEC Tech Consultant & Automation Expert | Bridging Software and Construction

外部链接
在 n8n.io 查看

分享此工作流