n8n_7_Revit和IFC碳足迹CO2估算器
高级
这是一个AI Summarization, Multimodal AI领域的自动化工作流,包含 55 个节点。主要使用 If, Set, Code, Merge, ManualTrigger 等节点。 使用AI分类计算Revit/IFC模型的隐含碳(CO2)
前置要求
- •OpenAI API Key
- •Anthropic API Key
使用的节点 (55)
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
"id": "UgJXCTdg8rea9Skt",
"meta": {
"instanceId": "faa70e11b7175129a74fd834d3451fdc1862589b16d68ded03f91ca7b1ecca12"
},
"name": "n8n_7_Revit和IFC碳足迹CO2估算器",
"tags": [],
"nodes": [
{
"id": "86512d4b-52b7-46ac-ac59-61ed748b3045",
"name": "查找分类字段1",
"type": "n8n-nodes-base.code",
"position": [
-80,
720
],
"parameters": {
"jsCode": "const items = $input.all();\nif (items.length === 0) {\n return [{json: {error: 'No grouped data found'}}];\n}\n\nconst headers = Object.keys(items[0].json);\n\nconst categoryPatterns = [\n { pattern: /^category$/i, type: 'Category' },\n { pattern: /^ifc[\\s_-]?type$/i, type: 'IFC' },\n { pattern: /^host[\\s_-]?category$/i, type: 'Host' },\n { pattern: /^ifc[\\s_-]?export[\\s_-]?as$/i, type: 'Export' },\n { pattern: /^layer$/i, type: 'Layer' }\n];\n\nlet categoryField = null;\nlet categoryFieldType = 'None';\n\n// Ищем поле категории\nfor (const header of headers) {\n for (const {pattern, type} of categoryPatterns) {\n if (pattern.test(header)) {\n categoryField = header;\n categoryFieldType = type;\n break;\n }\n }\n if (categoryField) break;\n}\n\nconst volumetricPatterns = /volume|area|length|count|quantity|thickness|perimeter|depth|size|dimension|weight|mass/i;\nconst volumetricFields = headers.filter(header => volumetricPatterns.test(header));\n\nconst categoryValues = new Set();\nif (categoryField) {\n items.forEach(item => {\n const value = item.json[categoryField];\n if (value && value !== '' && value !== null) {\n categoryValues.add(value);\n }\n });\n}\n\nconsole.log('Category field analysis:');\nconsole.log('- Field found:', categoryField || 'None');\nconsole.log('- Field type:', categoryFieldType);\nconsole.log('- Unique values:', categoryValues.size);\nconsole.log('- Volumetric fields:', volumetricFields.length);\n\nreturn [{\n json: {\n categoryField: categoryField,\n categoryFieldType: categoryFieldType,\n categoryValues: Array.from(categoryValues),\n volumetricFields: volumetricFields,\n groupedData: items.map(item => item.json),\n totalGroups: items.length\n }\n}];"
},
"typeVersion": 2
},
{
"id": "ceeaa6f3-781e-421f-bae1-c6ccb784ecbd",
"name": "对分组应用分类1",
"type": "n8n-nodes-base.code",
"position": [
448,
768
],
"parameters": {
"jsCode": "const categoryInfo = $node['Find Category Fields1'].json;\nconst groupedData = categoryInfo.groupedData;\nconst categoryField = categoryInfo.categoryField;\nconst volumetricFields = categoryInfo.volumetricFields || [];\n\nlet classifications = {};\nlet buildingCategories = [];\nlet drawingCategories = [];\n\ntry {\n const aiResponse = $input.first().json;\n const content = aiResponse.content || aiResponse.message || aiResponse.response || '';\n \n const jsonMatch = content.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n const parsed = JSON.parse(jsonMatch[0]);\n classifications = parsed.classifications || {};\n buildingCategories = parsed.building_categories || [];\n drawingCategories = parsed.drawing_categories || [];\n console.log(`AI classified ${Object.keys(classifications).length} categories`);\n console.log(`Building categories: ${buildingCategories.length}`);\n console.log(`Drawing categories: ${drawingCategories.length}`);\n }\n} catch (error) {\n console.error('Error parsing AI classification:', error.message);\n}\n\nreturn groupedData.map(group => {\n let isBuildingElement = false;\n let reason = '';\n let confidence = 0;\n \n if (categoryField && group[categoryField]) {\n const categoryValue = group[categoryField];\n \n if (classifications[categoryValue] !== undefined) {\n isBuildingElement = classifications[categoryValue];\n confidence = 95;\n reason = `Category '${categoryValue}' classified by AI as ${isBuildingElement ? 'building element' : 'drawing/annotation'}`;\n } else {\n \n const lowerCategory = categoryValue.toLowerCase();\n const drawingKeywords = /annotation|drawing|text|dimension|tag|view|sheet|grid|section|elevation|callout|revision|legend|symbol|mark|note|detail items|filled region|detail line/i;\n const buildingKeywords = /wall|floor|roof|column|beam|door|window|stair|pipe|duct|equipment|fixture|furniture/i;\n \n if (drawingKeywords.test(lowerCategory)) {\n isBuildingElement = false;\n confidence = 85;\n reason = `Category '${categoryValue}' matched drawing keywords`;\n } else if (buildingKeywords.test(lowerCategory)) {\n isBuildingElement = true;\n confidence = 85;\n reason = `Category '${categoryValue}' matched building keywords`;\n } else {\n // По умолчанию считаем строительным элементом если не очевидно обратное\n isBuildingElement = true;\n confidence = 70;\n reason = `Category '${categoryValue}' assumed as building element (no clear match)`;\n }\n }\n } else {\n // Если нет категории, проверяем наличие объемных параметров\n let hasSignificantVolumetricData = false;\n let volumetricCount = 0;\n \n for (const field of volumetricFields) {\n const value = parseFloat(group[field]);\n if (!isNaN(value) && value > 0) {\n hasSignificantVolumetricData = true;\n volumetricCount++;\n }\n }\n \n isBuildingElement = hasSignificantVolumetricData;\n confidence = hasSignificantVolumetricData ? 80 : 60;\n reason = hasSignificantVolumetricData ? \n `Has ${volumetricCount} volumetric parameters with values` : \n 'No category field and no significant volumetric data';\n }\n \n return {\n json: {\n ...group,\n is_building_element: isBuildingElement,\n element_confidence: confidence,\n element_reason: reason\n }\n };\n});"
},
"typeVersion": 2
},
{
"id": "ffc937c8-4ca6-4963-a24f-114f45b5345b",
"name": "非建筑元素输出1",
"type": "n8n-nodes-base.set",
"position": [
848,
784
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "message",
"name": "message",
"type": "string",
"value": "Non-building elements (drawings, annotations, etc.)"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "731a2f76-60b4-4b4c-8e56-465c0ea6ad5c",
"name": "提取标题和数据1",
"type": "n8n-nodes-base.code",
"position": [
80,
496
],
"parameters": {
"jsCode": " const items = $input.all();\n if (items.length === 0) {\n throw new Error('No data found in Excel file');\n }\n\n const allHeaders = new Set();\n items.forEach(item => {\n Object.keys(item.json).forEach(key => allHeaders.add(key));\n });\n\n const headers = Array.from(allHeaders);\n const cleanedHeaders = headers.map(header => {\n return header.replace(/:\\s*(string|double|int|float|boolean|number)\\s*$/i, '').trim();\n });\n\n const headerMapping = {};\n headers.forEach((oldHeader, index) => {\n headerMapping[oldHeader] = cleanedHeaders[index];\n });\n\n const sampleValues = {};\n cleanedHeaders.forEach((header, index) => {\n const originalHeader = headers[index];\n for (const item of items) {\n const value = item.json[originalHeader];\n if (value !== null && value !== undefined && value !== '') {\n sampleValues[header] = value;\n break;\n }\n }\n if (!sampleValues[header]) {\n sampleValues[header] = null;\n }\n });\n\n console.log(`Found ${headers.length} unique headers across ${items.length} items`);\n\n return [{\n json: {\n headers: cleanedHeaders,\n originalHeaders: headers,\n headerMapping: headerMapping,\n sampleValues: sampleValues,\n totalRows: items.length,\n totalHeaders: headers.length,\n rawData: items.map(item => item.json)\n }\n }];"
},
"typeVersion": 2
},
{
"id": "7bbf3d9d-4406-4fa3-9ca6-49eeaea551ff",
"name": "AI分析所有标题1",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
240,
496
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "chatgpt-4o-latest",
"cachedResultName": "CHATGPT-4O-LATEST"
},
"options": {
"temperature": 0.1
},
"messages": {
"values": [
{
"role": "system",
"content": "You are an expert in construction classification systems. Analyze building element groups and assign aggregation methods for grouping data.\n\nRules:\n1. 'sum' - for quantities that should be totaled:\n - Volume, Area, Length, Width, Height, Depth, Size\n - Count, Quantity, Number, Amount, Total\n - Thickness, Perimeter, Dimension\n - Weight, Mass, Load\n - Any measurable physical property that accumulates\n\n2. 'mean' (average) - for rates and unit values:\n - Price, Cost, Rate (per unit)\n - Coefficient, Factor, Ratio\n - Percentage, Percent\n - Efficiency, Performance metrics\n - Any per-unit or normalized values\n\n3. 'first' - for descriptive/categorical data:\n - ID, Code, Number (when used as identifier)\n - Name, Title, Description\n - Type, Category, Class, Group\n - Material, Component, Element\n - Project, Building, Location\n - Status, Phase, Stage\n - Any text or categorical field\n\nIMPORTANT: \n- Analyze each header carefully\n- Consider both the header name AND sample value\n- Return aggregation rule for EVERY header provided\n- Use exact header names from input\n\nReturn ONLY valid JSON in this exact format:\n{\n \"aggregation_rules\": {\n \"Header1\": \"sum\",\n \"Header2\": \"first\",\n \"Header3\": \"mean\"\n }\n}"
},
{
"content": "Analyze these {{ $json.totalHeaders }} headers and determine aggregation method for each:\n\nHeaders with sample values:\n{{ JSON.stringify($json.sampleValues, null, 2) }}\n\nProvide aggregation rule for EACH header listed above."
}
]
}
},
"credentials": {
"openAiApi": {
"id": "5SwKOx6OOukR6C0w",
"name": "OpenAi account n8n"
}
},
"typeVersion": 1.3
},
{
"id": "10caf6f7-ad6b-4de2-9e63-8b06b5609414",
"name": "处理AI响应1",
"type": "n8n-nodes-base.code",
"position": [
592,
496
],
"parameters": {
"jsCode": "// Обрабатываем ответ AI и применяем правила\n const aiResponse = $input.first().json;\n const headerData = $node['Extract Headers and Data1'].json;\n\n// Извлекаем правила из AI ответа\n let aiRules = {};\n try {\n const content = aiResponse.content || aiResponse.message || aiResponse.response || '';\n console.log('AI Response received, length:', content.length);\n \n // Ищем JSON в ответе\n const jsonMatch = content.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n const parsed = JSON.parse(jsonMatch[0]);\n aiRules = parsed.aggregation_rules || parsed.parameter_aggregation || {};\n console.log(`AI provided ${Object.keys(aiRules).length} rules`);\n } else {\n console.warn('No JSON found in AI response');\n }\n } catch (error) {\n console.error('Error parsing AI response:', error.message);\n }\n\n// Создаем финальные правила с дефолтами\n const finalRules = {};\n headerData.headers.forEach(header => {\n if (aiRules[header]) {\n finalRules[header] = aiRules[header];\n } else {\n // Применяем правила по умолчанию\n const lowerHeader = header.toLowerCase();\n \n if (lowerHeader.match(/volume|area|length|width|height|count|quantity|thickness|perimeter|depth|size|dimension|weight|mass|total|amount|number/)) {\n finalRules[header] = 'sum';\n } else if (lowerHeader.match(/price|rate|cost|coefficient|factor|percent|ratio|efficiency|avg|average|mean/)) {\n finalRules[header] = 'mean';\n } else {\n finalRules[header] = 'first';\n }\n }\n });\n\n const groupByParam = $node['Setup - Define file paths'].json.group_by;\n\n console.log(`\\nAggregation rules summary:`);\n console.log(`- Total headers: ${headerData.headers.length}`);\n console.log(`- AI rules: ${Object.keys(aiRules).length}`);\n console.log(`- Default rules: ${headerData.headers.length - Object.keys(aiRules).length}`);\n console.log(`- Group by: ${groupByParam}`);\n\n// Возвращаем все данные для группировки\n return [{\n json: {\n aggregationRules: finalRules,\n headerMapping: headerData.headerMapping,\n headers: headerData.headers,\n originalHeaders: headerData.originalHeaders,\n rawData: headerData.rawData,\n groupByParam: groupByParam,\n totalRows: headerData.totalRows\n }\n }];"
},
"typeVersion": 2
},
{
"id": "435f2cb9-9ccb-4f32-ba95-6bb0126a73d2",
"name": "使用AI规则分组数据1",
"type": "n8n-nodes-base.code",
"position": [
784,
496
],
"parameters": {
"jsCode": " const input = $input.first().json;\n const aggregationRules = input.aggregationRules;\n const headerMapping = input.headerMapping;\n const rawData = input.rawData;\n const groupByParamOriginal = input.groupByParam;\n\n const groupByParam = headerMapping[groupByParamOriginal] || groupByParamOriginal;\n\n console.log(`Grouping ${rawData.length} items by: ${groupByParam}`);\n\n const cleanedData = rawData.map(item => {\n const cleaned = {};\n Object.entries(item).forEach(([key, value]) => {\n const newKey = headerMapping[key] || key;\n cleaned[newKey] = value;\n });\n return cleaned;\n });\n\n const grouped = {};\n\n cleanedData.forEach(item => {\n const groupKey = item[groupByParam];\n \n if (!groupKey || groupKey === '' || groupKey === null) return;\n \n if (!grouped[groupKey]) {\n grouped[groupKey] = {\n _count: 0,\n _values: {}\n };\n \n Object.keys(aggregationRules).forEach(param => {\n if (param !== groupByParam) {\n grouped[groupKey]._values[param] = [];\n }\n });\n }\n \n grouped[groupKey]._count++;\n \n Object.entries(item).forEach(([key, value]) => {\n if (key === groupByParam) return;\n \n if (value !== null && value !== undefined && value !== '' && grouped[groupKey]._values[key]) {\n grouped[groupKey]._values[key].push(value);\n }\n });\n });\n\n const result = [];\n\n Object.entries(grouped).forEach(([groupKey, groupData]) => {\n const aggregated = {\n [groupByParam]: groupKey,\n 'Element Count': groupData._count\n };\n \n Object.entries(groupData._values).forEach(([param, values]) => {\n const rule = aggregationRules[param] || 'first';\n \n if (values.length === 0) {\n aggregated[param] = null;\n return;\n }\n \n switch(rule) {\n case 'sum':\n const numericValues = values.map(v => {\n const num = parseFloat(String(v).replace(',', '.'));\n return isNaN(num) ? 0 : num;\n });\n aggregated[param] = numericValues.reduce((a, b) => a + b, 0);\n // Округляем до 2 знаков после запятой если нужно\n if (aggregated[param] % 1 !== 0) {\n aggregated[param] = Math.round(aggregated[param] * 100) / 100;\n }\n break;\n \n case 'mean':\n case 'average':\n const avgValues = values.map(v => {\n const num = parseFloat(String(v).replace(',', '.'));\n return isNaN(num) ? null : num;\n }).filter(v => v !== null);\n \n if (avgValues.length > 0) {\n const avg = avgValues.reduce((a, b) => a + b, 0) / avgValues.length;\n aggregated[param] = Math.round(avg * 100) / 100;\n } else {\n aggregated[param] = values[0];\n }\n break;\n \n case 'first':\n default:\n aggregated[param] = values[0];\n break;\n }\n });\n \n result.push({ json: aggregated });\n });\n\n// Сортируем результат\n result.sort((a, b) => {\n const aVal = a.json[groupByParam];\n const bVal = b.json[groupByParam];\n if (aVal < bVal) return -1;\n if (aVal > bVal) return 1;\n return 0;\n });\n\n console.log(`\\nGrouping complete:`);\n console.log(`- Input items: ${cleanedData.length}`);\n console.log(`- Output groups: ${result.length}`);\n console.log(`- Parameters processed: ${Object.keys(aggregationRules).length}`);\n\n const rulesSummary = { sum: [], mean: [], first: [] };\n Object.entries(aggregationRules).forEach(([param, rule]) => {\n if (rulesSummary[rule]) rulesSummary[rule].push(param);\n });\n\n console.log('\\nAggregation summary:');\n if (rulesSummary.sum.length > 0) {\n console.log(`- SUM (${rulesSummary.sum.length}): ${rulesSummary.sum.slice(0, 5).join(', ')}${rulesSummary.sum.length > 5 ? '...' : ''}`);\n }\n if (rulesSummary.mean.length > 0) {\n console.log(`- MEAN (${rulesSummary.mean.length}): ${rulesSummary.mean.slice(0, 5).join(', ')}${rulesSummary.mean.length > 5 ? '...' : ''}`);\n }\n if (rulesSummary.first.length > 0) {\n console.log(`- FIRST (${rulesSummary.first.length}): ${rulesSummary.first.slice(0, 5).join(', ')}${rulesSummary.first.length > 5 ? '...' : ''}`);\n }\n\n return result;"
},
"typeVersion": 2
},
{
"id": "ff967309-0ef4-4b49-abf1-b4c3ff6d48ac",
"name": "AI分类类别1",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
112,
768
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "chatgpt-4o-latest",
"cachedResultName": "CHATGPT-4O-LATEST"
},
"options": {
"maxTokens": 4000,
"temperature": 0.1
},
"messages": {
"values": [
{
"role": "system",
"content": "You are an expert in Revit, BIM (Building Information Modeling) and construction classification. Your task is to classify category values as either building elements or non-building elements (drawings, annotations, etc.).\n\nBuilding elements include:\n- Structural elements (walls, floors, roofs, columns, beams, foundations, slabs)\n- MEP elements (pipes, ducts, equipment, fixtures, mechanical equipment)\n- Architectural elements (doors, windows, stairs, railings, curtain walls)\n- Site elements (parking, roads, landscaping)\n- Furniture and fixtures\n- Any physical construction element that has volume, area, or physical properties\n\nNon-building elements include:\n- Drawings and sheets\n- Annotations, dimensions, text notes\n- Views, sections, elevations, plans\n- Tags, symbols, legends, schedules\n- Grids, levels, reference planes\n- Revision clouds, callouts, detail items\n- Lines, filled regions, detail lines\n- Any 2D documentation or annotation element\n\nIMPORTANT: Analyze the actual category name, not just keywords. For example:\n- \"Detail Items\" = non-building (annotation)\n- \"Plumbing Fixtures\" = building element\n- \"Room Tags\" = non-building (annotation)\n- \"Structural Columns\" = building element\n\nReturn ONLY valid JSON in this exact format:\n{\n \"classifications\": {\n \"category_value_1\": true,\n \"category_value_2\": false\n },\n \"building_categories\": [\"list\", \"of\", \"building\", \"categories\"],\n \"drawing_categories\": [\"list\", \"of\", \"drawing\", \"categories\"]\n}"
},
{
"content": "{{ $json.categoryField ? `Classify these ${$json.categoryValues.length} category values from field '${$json.categoryField}' as building elements (true) or drawings/annotations (false):\n\nCategory values:\n${JSON.stringify($json.categoryValues, null, 2)}\n\nCategory field type: ${$json.categoryFieldType}` : 'No category field found. Please classify based on volumetric data presence.'}}"
}
]
}
},
"credentials": {
"openAiApi": {
"id": "5SwKOx6OOukR6C0w",
"name": "OpenAi account n8n"
}
},
"typeVersion": 1.3
},
{
"id": "3e9cd8b6-3117-493b-97f7-c9d6a889e55d",
"name": "是否为建筑元素1",
"type": "n8n-nodes-base.if",
"position": [
640,
768
],
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json.is_building_element }}",
"value2": true
}
]
}
},
"typeVersion": 1
},
{
"id": "1f3e2b55-6c9d-4466-801b-d8b31792a907",
"name": "检查所有批次是否完成",
"type": "n8n-nodes-base.if",
"position": [
-416,
1200
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "9af8b6d4-3a3a-4c4a-8d4d-d5a8c2958f5f",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $node['Process in Batches1'].context['noItemsLeft'] }}",
"rightValue": true
}
]
}
},
"typeVersion": 2
},
{
"id": "15da077e-8560-4bf9-b610-0199f66292f8",
"name": "收集所有结果",
"type": "n8n-nodes-base.code",
"position": [
-192,
1552
],
"parameters": {
"jsCode": "// Get all accumulated data and prepare for reporting\nconst storedData = $getWorkflowStaticData('global');\nconst allProcessedItems = storedData.accumulatedData || [];\n\nconsole.log(`\\n=== BATCH PROCESSING COMPLETE ===`);\nconsole.log(`Total items processed: ${allProcessedItems.length}`);\n\n// Clear accumulated data for next run\nstoredData.accumulatedData = [];\n\n// Return all items for report generation\nreturn allProcessedItems;"
},
"typeVersion": 2
},
{
"id": "7ae11a97-c8f9-4554-a1ae-0fc22c5ef6f9",
"name": "分批处理1",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-256,
1072
],
"parameters": {
"options": {},
"batchSize": 1
},
"typeVersion": 1
},
{
"id": "b2423ed4-6d42-4b31-a1c1-db9affbf720b",
"name": "清理空值1",
"type": "n8n-nodes-base.code",
"position": [
-80,
1072
],
"parameters": {
"jsCode": "// Clean empty values from the item\nreturn $input.all().map(item => {\n const cleanedJson = {};\n Object.entries(item.json).forEach(([key, value]) => {\n if (value !== null) {\n if (typeof value === 'number') {\n if (value !== 0 || key === 'Element Count') {\n cleanedJson[key] = value;\n }\n } else if (value !== '') {\n cleanedJson[key] = value;\n }\n }\n });\n return { json: cleanedJson };\n});"
},
"typeVersion": 2
},
{
"id": "b9e77fc6-907b-47d2-8625-fdc0f6febb56",
"name": "AI代理增强",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
272,
1072
],
"parameters": {
"text": "={{ $json.userPrompt }}",
"options": {
"systemMessage": "={{ $json.systemPrompt }}"
},
"promptType": "define"
},
"typeVersion": 1.7
},
{
"id": "2c3a57c2-66c2-414a-82e0-8f0df26cbfc8",
"name": "累积结果",
"type": "n8n-nodes-base.code",
"position": [
848,
1216
],
"parameters": {
"jsCode": "// Accumulate all processed items\nconst currentItem = $input.first();\nconst storedData = $getWorkflowStaticData('global');\n\n// Initialize accumulator if needed\nif (!storedData.accumulatedData) {\n storedData.accumulatedData = [];\n}\n\n// Add current item to accumulated data\nstoredData.accumulatedData.push(currentItem);\n\nconsole.log(`Item processed. Total accumulated: ${storedData.accumulatedData.length} items`);\n\n// Return the current item to continue the flow\nreturn [currentItem];"
},
"typeVersion": 2
},
{
"id": "fbf5d0a1-ed19-49e0-9a6e-27bf506bedec",
"name": "Anthropic聊天模型1",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"position": [
160,
1264
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "claude-opus-4-20250514",
"cachedResultName": "Claude Opus 4"
},
"options": {}
},
"credentials": {
"anthropicApi": {
"id": "af7za1FRO2jVYY9L",
"name": "Anthropic account"
}
},
"typeVersion": 1.3
},
{
"id": "e7df68eb-4420-48b5-8465-b80c49f0122f",
"name": "计算项目总计4",
"type": "n8n-nodes-base.code",
"position": [
-16,
1552
],
"parameters": {
"jsCode": "// Aggregate all results and calculate project totals\nconst items = $input.all();\n\n// Initialize aggregators\nconst projectTotals = {\n totalElements: 0,\n totalMass: 0,\n totalCO2: 0,\n totalVolume: 0,\n totalArea: 0,\n byMaterial: {},\n byCategory: {},\n byImpact: {}\n};\n\n// Process each item\nitems.forEach(item => {\n const data = item.json;\n const elementCount = parseFloat(data['Element Count']) || 1;\n const mass = parseFloat(data['Element Mass (tonnes)']) || 0;\n const co2 = parseFloat(data['Total CO2 (tonnes CO2e)']) || 0;\n const volume = parseFloat(data['Volume (m³)']) || 0;\n const area = parseFloat(data['Area (m²)']) || 0;\n const material = data['Material (EU Standard)'] || 'Unknown';\n const category = data['Element Category'] || 'Unknown';\n const impact = data['Impact Category'] || 'Unknown';\n \n // Update totals\n projectTotals.totalElements += elementCount;\n projectTotals.totalMass += mass;\n projectTotals.totalCO2 += co2;\n projectTotals.totalVolume += volume;\n projectTotals.totalArea += area;\n \n // Aggregate by material\n if (!projectTotals.byMaterial[material]) {\n projectTotals.byMaterial[material] = {\n elements: 0,\n mass: 0,\n co2: 0,\n volume: 0,\n types: new Set()\n };\n }\n projectTotals.byMaterial[material].elements += elementCount;\n projectTotals.byMaterial[material].mass += mass;\n projectTotals.byMaterial[material].co2 += co2;\n projectTotals.byMaterial[material].volume += volume;\n projectTotals.byMaterial[material].types.add(data['Element Name']);\n \n // Aggregate by category\n if (!projectTotals.byCategory[category]) {\n projectTotals.byCategory[category] = {\n elements: 0,\n co2: 0\n };\n }\n projectTotals.byCategory[category].elements += elementCount;\n projectTotals.byCategory[category].co2 += co2;\n \n // Aggregate by impact\n if (!projectTotals.byImpact[impact]) {\n projectTotals.byImpact[impact] = {\n elements: 0,\n co2: 0\n };\n }\n projectTotals.byImpact[impact].elements += elementCount;\n projectTotals.byImpact[impact].co2 += co2;\n});\n\n// Add percentages and rankings to each item\nconst enrichedItems = items.map((item, index) => {\n const data = item.json;\n const co2 = parseFloat(data['Total CO2 (tonnes CO2e)']) || 0;\n const mass = parseFloat(data['Element Mass (tonnes)']) || 0;\n const elementCount = parseFloat(data['Element Count']) || 1;\n \n return {\n json: {\n ...data,\n // Add project percentages\n 'CO2 % of Total': projectTotals.totalCO2 > 0 ? \n ((co2 / projectTotals.totalCO2) * 100).toFixed(2) : '0.00',\n 'Mass % of Total': projectTotals.totalMass > 0 ? \n ((mass / projectTotals.totalMass) * 100).toFixed(2) : '0.00',\n 'Elements % of Total': projectTotals.totalElements > 0 ? \n ((elementCount / projectTotals.totalElements) * 100).toFixed(2) : '0.00',\n // Add ranking\n 'CO2 Rank': index + 1,\n // Project totals (same for all rows)\n 'Project Total Elements': projectTotals.totalElements,\n 'Project Total Mass (tonnes)': projectTotals.totalMass.toFixed(3),\n 'Project Total CO2 (tonnes)': projectTotals.totalCO2.toFixed(3),\n 'Project Total Volume (m³)': projectTotals.totalVolume.toFixed(2),\n 'Project Total Area (m²)': projectTotals.totalArea.toFixed(2)\n }\n };\n});\n\n// Sort by CO2 emissions (highest first)\nenrichedItems.sort((a, b) => \n parseFloat(b.json['Total CO2 (tonnes CO2e)']) - parseFloat(a.json['Total CO2 (tonnes CO2e)'])\n);\n\n// Store aggregated data for summary\n$getWorkflowStaticData('global').projectTotals = projectTotals;\n\nreturn enrichedItems;"
},
"typeVersion": 2
},
{
"id": "82b3f0eb-07d8-4a97-82a0-bbc38c7d040a",
"name": "增强Excel输出",
"type": "n8n-nodes-base.code",
"position": [
528,
1712
],
"parameters": {
"jsCode": "// Enhanced Excel styling configuration\nconst excelBuffer = $input.first().binary.data;\nconst fileName = `CO2_Analysis_Professional_Report_${new Date().toISOString().slice(0,10)}.xlsx`;\n\n// Add metadata to the file\nconst metadata = {\n title: 'Carbon Footprint Analysis Report',\n author: 'DataDrivenConstruction.io',\n company: 'Automated CO2 Analysis System',\n created: new Date().toISOString(),\n description: 'Comprehensive embodied carbon assessment with multi-standard material classification',\n keywords: 'CO2, Carbon Footprint, Embodied Carbon, LCA, Building Materials'\n};\n\n// Return the enhanced Excel file\nreturn [{\n json: {\n fileName: fileName,\n fileSize: excelBuffer.data.length,\n sheets: 8,\n metadata: metadata,\n timestamp: new Date().toISOString()\n },\n binary: {\n data: {\n ...excelBuffer,\n fileName: fileName,\n fileExtension: 'xlsx',\n mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'\n }\n }\n}];"
},
"typeVersion": 2
},
{
"id": "4d379a3c-ab28-450b-a7a1-0eb28b706376",
"name": "准备Excel数据",
"type": "n8n-nodes-base.code",
"position": [
192,
1712
],
"parameters": {
"jsCode": "// Prepare comprehensive data for multi-sheet Excel export\nconst items = $input.all();\nconst projectTotals = $getWorkflowStaticData('global').projectTotals;\n\n// Helper function to format numbers\nconst formatNumber = (num, decimals = 2) => {\n return typeof num === 'number' ? parseFloat(num.toFixed(decimals)) : 0;\n};\n\n// Sheet 1: Executive Summary with project metrics\nconst executiveSummary = [\n {\n 'Category': 'PROJECT OVERVIEW',\n 'Metric': 'Total Elements Analyzed',\n 'Value': projectTotals.totalElements,\n 'Unit': 'elements',\n 'Benchmark': 'N/A',\n 'Status': '✓',\n 'Notes': 'All building elements included in analysis'\n },\n {\n 'Category': 'PROJECT OVERVIEW',\n 'Metric': 'Element Groups',\n 'Value': items.length,\n 'Unit': 'groups',\n 'Benchmark': 'N/A',\n 'Status': '✓',\n 'Notes': 'Grouped by element type'\n },\n {\n 'Category': 'CARBON METRICS',\n 'Metric': 'Total Embodied Carbon',\n 'Value': formatNumber(projectTotals.totalCO2, 2),\n 'Unit': 'tonnes CO2e',\n 'Benchmark': 'Industry avg: ' + formatNumber(projectTotals.totalCO2 * 1.2, 0),\n 'Status': projectTotals.totalCO2 < projectTotals.totalCO2 * 1.2 ? '✓' : '⚠',\n 'Notes': 'A1-A3 lifecycle stages only'\n },\n {\n 'Category': 'CARBON METRICS',\n 'Metric': 'Average Carbon Intensity',\n 'Value': formatNumber(projectTotals.totalCO2 / projectTotals.totalMass, 3),\n 'Unit': 'kg CO2e/kg',\n 'Benchmark': '1.5-2.5',\n 'Status': (projectTotals.totalCO2 / projectTotals.totalMass) < 2.5 ? '✓' : '⚠',\n 'Notes': 'Average across all materials'\n },\n {\n 'Category': 'MATERIAL METRICS',\n 'Metric': 'Total Material Mass',\n 'Value': formatNumber(projectTotals.totalMass, 2),\n 'Unit': 'tonnes',\n 'Benchmark': 'N/A',\n 'Status': '✓',\n 'Notes': 'Combined mass of all materials'\n },\n {\n 'Category': 'MATERIAL METRICS',\n 'Metric': 'Unique Material Types',\n 'Value': Object.keys(projectTotals.byMaterial).length,\n 'Unit': 'materials',\n 'Benchmark': '10-20',\n 'Status': '✓',\n 'Notes': 'Distinct material classifications'\n },\n {\n 'Category': 'VOLUMETRIC DATA',\n 'Metric': 'Total Volume',\n 'Value': formatNumber(projectTotals.totalVolume, 2),\n 'Unit': 'm³',\n 'Benchmark': 'N/A',\n 'Status': '✓',\n 'Notes': 'Where volume data available'\n },\n {\n 'Category': 'VOLUMETRIC DATA',\n 'Metric': 'Total Area',\n 'Value': formatNumber(projectTotals.totalArea, 2),\n 'Unit': 'm²',\n 'Benchmark': 'N/A',\n 'Status': '✓',\n 'Notes': 'Where area data available'\n }\n];\n\n// Sheet 2: Detailed Elements Analysis - ALL fields from processing\nconst detailedElements = items.map((item, index) => {\n const data = item.json;\n return {\n // Ranking and identification\n 'CO2_Rank': index + 1,\n 'Element_Group': data['Element Name'] || data['Type Name'] || 'Unknown',\n 'Element_Category': data['Element Category'] || 'Unknown',\n 'Element_Type': data['Element Type'] || 'Unknown',\n 'Element_Function': data['Element Function'] || 'Unknown',\n 'Element_Count': parseInt(data['Element Count']) || 0,\n \n // Material classification (all 3 standards)\n 'Material_EU_Standard': data['Material (EU Standard)'] || 'Unknown',\n 'Material_DE_Standard': data['Material (DE Standard)'] || 'Unknown',\n 'Material_US_Standard': data['Material (US Standard)'] || 'Unknown',\n 'Primary_Material': data['Primary Material'] || 'Unknown',\n 'Secondary_Materials': data['Secondary Materials'] || 'None',\n \n // Quantities and dimensions\n 'Quantity_Value': formatNumber(data['Quantity'] || 0, 3),\n 'Quantity_Unit': data['Quantity Unit'] || 'piece',\n 'Volume_m3': formatNumber(data['Volume (m³)'] || 0, 3),\n 'Area_m2': formatNumber(data['Area (m²)'] || 0, 3),\n 'Length_mm': formatNumber(data['Length (mm)'] || 0, 0),\n 'Width_mm': formatNumber(data['Width (mm)'] || 0, 0),\n 'Height_mm': formatNumber(data['Height (mm)'] || 0, 0),\n 'Thickness_mm': formatNumber(data['Thickness (mm)'] || 0, 0),\n \n // Mass and density\n 'Material_Density_kg_m3': formatNumber(data['Material Density (kg/m³)'] || 0, 0),\n 'Element_Mass_kg': formatNumber(data['Element Mass (kg)'] || 0, 2),\n 'Element_Mass_tonnes': formatNumber(data['Element Mass (tonnes)'] || 0, 3),\n \n // CO2 emissions data\n 'CO2_Factor_kg_CO2_per_kg': formatNumber(data['CO2 Factor (kg CO2e/kg)'] || 0, 3),\n 'Total_CO2_kg': formatNumber(data['Total CO2 (kg CO2e)'] || 0, 2),\n 'Total_CO2_tonnes': formatNumber(data['Total CO2 (tonnes CO2e)'] || 0, 3),\n 'CO2_per_Element_kg': formatNumber(data['CO2 per Element (kg CO2e)'] || 0, 2),\n 'CO2_Intensity': formatNumber(data['CO2 Intensity'] || 0, 3),\n 'CO2_Percent_of_Total': formatNumber(data['CO2 % of Total'] || 0, 2),\n \n // Impact and quality metrics\n 'Impact_Category': data['Impact Category'] || 'Unknown',\n 'Lifecycle_Stage': data['Lifecycle Stage'] || 'A1-A3',\n 'Data_Source': data['Data Source'] || 'Industry average',\n \n // Confidence scores\n 'Overall_Confidence_%': parseInt(data['Overall Confidence (%)']) || 0,\n 'Material_Confidence_%': parseInt(data['Material Confidence (%)']) || 0,\n 'Quantity_Confidence_%': parseInt(data['Quantity Confidence (%)']) || 0,\n 'CO2_Confidence_%': parseInt(data['CO2 Confidence (%)']) || 0,\n 'Data_Quality': data['Data Quality'] || 'unknown',\n \n // Analysis metadata\n 'Calculation_Method': data['Calculation Method'] || 'Not specified',\n 'Assumptions': data['Assumptions'] || 'None',\n 'Warnings': data['Warnings'] || 'None',\n 'Analysis_Notes': data['Analysis Notes'] || '',\n 'Processing_Timestamp': data['Processing Timestamp'] || new Date().toISOString(),\n 'Analysis_Status': data['Analysis Status'] || 'Unknown'\n };\n});\n\n// Sheet 3: Material Summary with detailed breakdown\nconst materialSummary = Object.entries(projectTotals.byMaterial)\n .sort((a, b) => b[1].co2 - a[1].co2)\n .map(([material, data], index) => {\n const co2Percent = (data.co2 / projectTotals.totalCO2) * 100;\n const massPercent = (data.mass / projectTotals.totalMass) * 100;\n const avgCO2Factor = data.mass > 0 ? data.co2 / data.mass : 0;\n \n return {\n 'Rank': index + 1,\n 'Material_Type': material,\n 'Element_Count': data.elements,\n 'Unique_Types': data.types ? data.types.size : 0,\n 'Mass_tonnes': formatNumber(data.mass, 2),\n 'Mass_%': formatNumber(massPercent, 1),\n 'Volume_m3': formatNumber(data.volume, 2),\n 'CO2_tonnes': formatNumber(data.co2, 2),\n 'CO2_%': formatNumber(co2Percent, 1),\n 'Avg_CO2_Factor': formatNumber(avgCO2Factor, 3),\n 'CO2_per_Element_kg': formatNumber(data.co2 / data.elements * 1000, 1),\n 'Impact_Level': co2Percent >= 20 ? 'CRITICAL' : co2Percent >= 10 ? 'HIGH' : co2Percent >= 5 ? 'MEDIUM' : 'LOW',\n 'Reduction_Potential_20%': formatNumber(data.co2 * 0.2, 2),\n 'Benchmark_Factor': formatNumber(avgCO2Factor * 0.8, 3)\n };\n });\n\n// Sheet 4: Category Analysis\nconst categoryAnalysis = Object.entries(projectTotals.byCategory)\n .sort((a, b) => b[1].co2 - a[1].co2)\n .map(([category, data], index) => {\n const co2Percent = (data.co2 / projectTotals.totalCO2) * 100;\n const elementsPercent = (data.elements / projectTotals.totalElements) * 100;\n \n return {\n 'Rank': index + 1,\n 'Category': category,\n 'Element_Count': data.elements,\n 'Elements_%': formatNumber(elementsPercent, 1),\n 'CO2_tonnes': formatNumber(data.co2, 2),\n 'CO2_%': formatNumber(co2Percent, 1),\n 'Avg_CO2_per_Element': formatNumber(data.co2 / data.elements * 1000, 1),\n 'CO2_Intensity_Ratio': formatNumber((data.co2 / data.elements) / (projectTotals.totalCO2 / projectTotals.totalElements), 2),\n 'Priority': co2Percent >= 15 ? 'HIGH' : co2Percent >= 5 ? 'MEDIUM' : 'LOW'\n };\n });\n\n// Sheet 5: Impact Analysis by Category\nconst impactAnalysis = Object.entries(projectTotals.byImpact || {})\n .map(([impact, data]) => ({\n 'Impact_Category': impact,\n 'Element_Count': data.elements,\n 'CO2_tonnes': formatNumber(data.co2, 2),\n 'CO2_%': formatNumber((data.co2 / projectTotals.totalCO2) * 100, 1),\n 'Avg_CO2_per_Element': formatNumber(data.co2 / data.elements * 1000, 1)\n }));\n\n// Sheet 6: Top 20 Hotspots with action items\nconst top20Hotspots = items\n .slice(0, 20)\n .map((item, index) => {\n const data = item.json;\n const co2Tonnes = parseFloat(data['Total CO2 (tonnes CO2e)']) || 0;\n const co2Percent = parseFloat(data['CO2 % of Total']) || 0;\n const elementCount = parseInt(data['Element Count']) || 1;\n \n // Generate specific recommendations based on material and impact\n let recommendation = '';\n if (co2Percent >= 10) {\n recommendation = 'CRITICAL: Prioritize immediate material substitution or design optimization';\n } else if (co2Percent >= 5) {\n recommendation = 'HIGH: Evaluate low-carbon alternatives and quantity reduction opportunities';\n } else if (co2Percent >= 2) {\n recommendation = 'MEDIUM: Consider optimization during value engineering phase';\n } else {\n recommendation = 'LOW: Monitor and optimize if convenient';\n }\n \n return {\n 'Priority_Rank': index + 1,\n 'Element_Group': data['Element Name'] || 'Unknown',\n 'Category': data['Element Category'] || 'Unknown',\n 'Material': data['Material (EU Standard)'] || 'Unknown',\n 'Element_Count': elementCount,\n 'Mass_tonnes': formatNumber(data['Element Mass (tonnes)'] || 0, 2),\n 'CO2_tonnes': formatNumber(co2Tonnes, 3),\n 'CO2_%': formatNumber(co2Percent, 2),\n 'CO2_per_Element': formatNumber(co2Tonnes / elementCount * 1000, 1),\n 'Impact_Level': co2Percent >= 10 ? 'CRITICAL' : co2Percent >= 5 ? 'HIGH' : co2Percent >= 2 ? 'MEDIUM' : 'LOW',\n 'Confidence_%': parseInt(data['Overall Confidence (%)']) || 0,\n 'Reduction_Target_20%': formatNumber(co2Tonnes * 0.2, 2),\n 'Recommendation': recommendation\n };\n });\n\n// Sheet 7: Data Quality Report\nconst dataQuality = items.map((item, index) => {\n const data = item.json;\n return {\n 'Element_Rank': index + 1,\n 'Element_Group': data['Element Name'] || 'Unknown',\n 'Overall_Confidence_%': parseInt(data['Overall Confidence (%)']) || 0,\n 'Material_Confidence_%': parseInt(data['Material Confidence (%)']) || 0,\n 'Quantity_Confidence_%': parseInt(data['Quantity Confidence (%)']) || 0,\n 'CO2_Confidence_%': parseInt(data['CO2 Confidence (%)']) || 0,\n 'Data_Quality': data['Data Quality'] || 'unknown',\n 'Data_Source': data['Data Source'] || 'Unknown',\n 'Assumptions': data['Assumptions'] || 'None',\n 'Warnings': data['Warnings'] || 'None',\n 'Analysis_Status': data['Analysis Status'] || 'Unknown'\n };\n}).filter(item => \n item['Overall_Confidence_%'] < 90 || \n item['Data_Quality'] !== 'high' || \n item['Warnings'] !== 'None'\n);\n\n// Sheet 8: Recommendations Summary\nconst recommendations = [\n {\n 'Priority': 1,\n 'Category': 'IMMEDIATE ACTIONS',\n 'Recommendation': `Focus on ${Object.entries(projectTotals.byMaterial).sort((a,b) => b[1].co2 - a[1].co2)[0][0]} optimization`,\n 'Potential_Savings': formatNumber(Object.entries(projectTotals.byMaterial).sort((a,b) => b[1].co2 - a[1].co2)[0][1].co2 * 0.2, 1) + ' tonnes CO2e',\n 'Implementation': 'Material substitution or design optimization',\n 'Timeline': '0-3 months'\n },\n {\n 'Priority': 2,\n 'Category': 'IMMEDIATE ACTIONS',\n 'Recommendation': `Review top ${items.filter(item => parseFloat(item.json['CO2 % of Total']) >= 5).length} high-impact element groups`,\n 'Potential_Savings': formatNumber(projectTotals.totalCO2 * 0.15, 1) + ' tonnes CO2e',\n 'Implementation': 'Design review and value engineering',\n 'Timeline': '0-3 months'\n },\n {\n 'Priority': 3,\n 'Category': 'SHORT TERM',\n 'Recommendation': 'Implement low-carbon concrete mixes where applicable',\n 'Potential_Savings': '10-15% reduction possible',\n 'Implementation': 'Specification updates',\n 'Timeline': '3-6 months'\n },\n {\n 'Priority': 4,\n 'Category': 'SHORT TERM',\n 'Recommendation': 'Increase recycled content in steel elements',\n 'Potential_Savings': '20-30% reduction possible',\n 'Implementation': 'Supplier engagement',\n 'Timeline': '3-6 months'\n },\n {\n 'Priority': 5,\n 'Category': 'MEDIUM TERM',\n 'Recommendation': 'Explore timber alternatives for suitable applications',\n 'Potential_Savings': 'Carbon negative potential',\n 'Implementation': 'Structural analysis required',\n 'Timeline': '6-12 months'\n }\n];\n\n// Create worksheet structure with proper sheet names\nconst worksheets = [\n { name: 'Executive Summary', data: executiveSummary },\n { name: 'All Elements', data: detailedElements },\n { name: 'Material Summary', data: materialSummary },\n { name: 'Category Analysis', data: categoryAnalysis },\n { name: 'Impact Analysis', data: impactAnalysis },\n { name: 'Top 20 Hotspots', data: top20Hotspots },\n { name: 'Data Quality', data: dataQuality },\n { name: 'Recommendations', data: recommendations }\n];\n\n// Flatten all data with sheet markers\nconst allData = [];\nworksheets.forEach(sheet => {\n sheet.data.forEach(row => {\n allData.push({\n json: {\n ...row,\n _sheetName: sheet.name\n }\n });\n });\n});\n\nreturn allData;"
},
"typeVersion": 2
},
{
"id": "a4d43d4c-ef26-4d96-b8df-88cdff94a9b3",
"name": "创建Excel文件",
"type": "n8n-nodes-base.spreadsheetFile",
"position": [
352,
1712
],
"parameters": {
"options": {
"fileName": "=CO2_Analysis_Report_{{ $now.format('yyyy-MM-dd') }}",
"headerRow": true,
"sheetName": "={{ $json._sheetName }}"
},
"operation": "toFile",
"fileFormat": "xlsx"
},
"typeVersion": 2
},
{
"id": "36bb02f7-baba-48c7-aad7-4fe2b057e6be",
"name": "准备增强提示",
"type": "n8n-nodes-base.code",
"position": [
96,
1072
],
"parameters": {
"jsCode": "// Enhanced prompts for comprehensive CO2 analysis\nconst inputData = $input.first().json;\nconst originalGroupedData = { ...inputData };\n\nconst systemPrompt = `You are an expert in construction materials, carbon footprint analysis, and building element classification. Analyze the provided building element data and return a comprehensive CO2 emissions assessment.\n\nIMPORTANT NOTE ON DATA:\n- All quantitative fields like Volume, Area, Length, etc. in the input data represent ALREADY AGGREGATED TOTALS for the entire group of elements.\n- 'Element Count' indicates the number of individual elements in this group.\n- DO NOT multiply volumes/areas by Element Count - they are already total sums.\n- Use the provided totals directly for mass and CO2 calculations.\n- If no volumetric data is available, estimate based on typical values, but prioritize provided data.\n\n## Your Analysis Must Include:\n\n### 1. Element Identification\n- Element type and category\n- Primary material composition\n- Secondary materials if applicable\n- Functional classification\n\n### 2. Material Classification (All 3 Standards)\n- **European (EN 15978/15804)**: Concrete/Steel/Wood/Glass/Insulation/etc.\n- **German (ÖKOBAUDAT)**: Mineralische/Metalle/Holz/Dämmstoffe/etc.\n- **US (MasterFormat)**: Division classifications\n\n### 3. Quantity Analysis\n- Primary quantity with unit (m³, m², m, kg, pieces) - use aggregated total from input\n- Calculation method used (e.g., 'Direct from provided total volume')\n- Confidence level (0-100%)\n- Data quality assessment\n\n### 4. CO2 Emissions Calculation\n- Material density (kg/m³)\n- Mass calculation: Use total volume/area * density (do not multiply by count)\n- CO2 emission factor (kg CO2e/kg)\n- Total CO2 emissions (for the entire group)\n- Emission intensity metrics\n\n### 5. Data Quality & Confidence\n- Overall confidence score\n- Data completeness assessment\n- Key assumptions made\n- Warnings or limitations\n\n## Important Guidelines:\n1. Use industry-standard emission factors\n2. Apply conservative estimates when uncertain\n3. Consider full lifecycle emissions (A1-A3 minimum)\n4. Account for regional variations where relevant\n5. Include embodied carbon only (not operational)\n\n## Output Format\nReturn ONLY valid JSON with this exact structure:\n{\n \"element_identification\": {\n \"name\": \"string\",\n \"category\": \"string\",\n \"type\": \"string\",\n \"function\": \"string\"\n },\n \"material_classification\": {\n \"european\": \"string\",\n \"german\": \"string\",\n \"us\": \"string\",\n \"primary_material\": \"string\",\n \"secondary_materials\": [\"string\"]\n },\n \"quantities\": {\n \"value\": number,\n \"unit\": \"string\",\n \"calculation_method\": \"string\",\n \"raw_dimensions\": {\n \"length\": number,\n \"width\": number,\n \"height\": number,\n \"thickness\": number,\n \"area\": number,\n \"volume\": number\n }\n },\n \"co2_analysis\": {\n \"density_kg_m3\": number,\n \"mass_kg\": number,\n \"co2_factor_kg_co2_per_kg\": number,\n \"total_co2_kg\": number,\n \"co2_intensity_kg_per_unit\": number,\n \"lifecycle_stage\": \"A1-A3\",\n \"data_source\": \"string\"\n },\n \"confidence\": {\n \"overall_score\": number,\n \"material_confidence\": number,\n \"quantity_confidence\": number,\n \"co2_confidence\": number,\n \"data_quality\": \"high/medium/low\"\n },\n \"metadata\": {\n \"assumptions\": [\"string\"],\n \"warnings\": [\"string\"],\n \"notes\": \"string\"\n }\n}`;\n\nconst userPrompt = `Analyze this building element group for CO2 emissions. Remember: Volumes and areas are already total for the group, not per element.\n\n${JSON.stringify(inputData, null, 2)}\n\nProvide comprehensive CO2 analysis following the specified format. Focus on accuracy and use conservative estimates where data is uncertain.`;\n\nreturn [{\n json: {\n ...originalGroupedData,\n systemPrompt,\n userPrompt,\n _originalGroupedData: originalGroupedData\n }\n}];"
},
"typeVersion": 2
},
{
"id": "32640ecd-b337-4745-bbe2-072e60163dcf",
"name": "解析增强响应",
"type": "n8n-nodes-base.code",
"position": [
624,
1072
],
"parameters": {
"jsCode": "// Parse and enrich AI response with all necessary data\nconst aiResponse = $input.first().json.output || $input.first().json.response || $input.first().json.text || $input.first().json;\nconst originalData = $node[\"Prepare Enhanced Prompts\"].json._originalGroupedData || $node[\"Prepare Enhanced Prompts\"].json;\n\ntry {\n // Extract JSON from response\n let jsonStr = aiResponse;\n if (typeof jsonStr === 'string') {\n const jsonMatch = jsonStr.match(/```json\\n?([\\s\\S]*?)\\n?```/) || jsonStr.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n jsonStr = jsonMatch[1] || jsonMatch[0];\n }\n }\n \n const analysis = typeof jsonStr === 'string' ? JSON.parse(jsonStr) : jsonStr;\n \n // Calculate additional metrics - use total CO2 for group\n const co2_kg = analysis.co2_analysis?.total_co2_kg || 0;\n const co2_tonnes = co2_kg / 1000;\n const elementCount = parseFloat(originalData['Element Count']) || 1;\n const co2_per_element = co2_kg / elementCount;\n \n // Determine impact category\n let impactCategory = 'Unknown';\n const co2Factor = analysis.co2_analysis?.co2_factor_kg_co2_per_kg || 0;\n if (co2Factor < 0) {\n impactCategory = 'Carbon Negative (Storage)';\n } else if (co2Factor <= 0.5) {\n impactCategory = 'Very Low Impact';\n } else if (co2Factor <= 1.0) {\n impactCategory = 'Low Impact';\n } else if (co2Factor <= 2.0) {\n impactCategory = 'Medium Impact';\n } else if (co2Factor <= 5.0) {\n impactCategory = 'High Impact';\n } else {\n impactCategory = 'Very High Impact';\n }\n \n // Create comprehensive output record\n return [{\n json: {\n // Original data\n ...originalData,\n \n // Element identification\n 'Element Name': analysis.element_identification?.name || originalData['Type Name'] || 'Unknown',\n 'Element Category': analysis.element_identification?.category || originalData['Category'] || 'Unknown',\n 'Element Type': analysis.element_identification?.type || 'Unknown',\n 'Element Function': analysis.element_identification?.function || 'Unknown',\n \n // Material classification\n 'Material (EU Standard)': analysis.material_classification?.european || 'Unknown',\n 'Material (DE Standard)': analysis.material_classification?.german || 'Unknown',\n 'Material (US Standard)': analysis.material_classification?.us || 'Unknown',\n 'Primary Material': analysis.material_classification?.primary_material || 'Unknown',\n 'Secondary Materials': (analysis.material_classification?.secondary_materials || []).join(', ') || 'None',\n \n // Quantities\n 'Quantity': analysis.quantities?.value || 0,\n 'Quantity Unit': analysis.quantities?.unit || 'piece',\n 'Calculation Method': analysis.quantities?.calculation_method || 'Not specified',\n \n // Dimensions (from raw data) - these are totals\n 'Length (mm)': analysis.quantities?.raw_dimensions?.length || originalData['Length'] || 0,\n 'Width (mm)': analysis.quantities?.raw_dimensions?.width || originalData['Width'] || 0,\n 'Height (mm)': analysis.quantities?.raw_dimensions?.height || originalData['Height'] || 0,\n 'Thickness (mm)': analysis.quantities?.raw_dimensions?.thickness || originalData['Thickness'] || 0,\n 'Area (m²)': analysis.quantities?.raw_dimensions?.area || originalData['Area'] || 0,\n 'Volume (m³)': analysis.quantities?.raw_dimensions?.volume || originalData['Volume'] || 0,\n \n // CO2 Analysis\n 'Material Density (kg/m³)': analysis.co2_analysis?.density_kg_m3 || 0,\n 'Element Mass (kg)': analysis.co2_analysis?.mass_kg || 0,\n 'Element Mass (tonnes)': (analysis.co2_analysis?.mass_kg || 0) / 1000,\n 'CO2 Factor (kg CO2e/kg)': analysis.co2_analysis?.co2_factor_kg_co2_per_kg || 0,\n 'Total CO2 (kg CO2e)': co2_kg,\n 'Total CO2 (tonnes CO2e)': co2_tonnes,\n 'CO2 per Element (kg CO2e)': co2_per_element,\n 'CO2 Intensity': analysis.co2_analysis?.co2_intensity_kg_per_unit || 0,\n 'Lifecycle Stage': analysis.co2_analysis?.lifecycle_stage || 'A1-A3',\n 'Data Source': analysis.co2_analysis?.data_source || 'Industry average',\n 'Impact Category': impactCategory,\n \n // Confidence scores\n 'Overall Confidence (%)': analysis.confidence?.overall_score || 0,\n 'Material Confidence (%)': analysis.confidence?.material_confidence || 0,\n 'Quantity Confidence (%)': analysis.confidence?.quantity_confidence || 0,\n 'CO2 Confidence (%)': analysis.confidence?.co2_confidence || 0,\n 'Data Quality': analysis.confidence?.data_quality || 'unknown',\n \n // Metadata\n 'Assumptions': (analysis.metadata?.assumptions || []).join('; ') || 'None',\n 'Warnings': (analysis.metadata?.warnings || []).join('; ') || 'None',\n 'Analysis Notes': analysis.metadata?.notes || '',\n 'Processing Timestamp': new Date().toISOString(),\n 'Analysis Status': 'Complete'\n }\n }];\n \n} catch (error) {\n // Return error record with original data preserved\n return [{\n json: {\n ...originalData,\n 'Analysis Status': 'Failed',\n 'Error': error.message,\n 'Processing Timestamp': new Date().toISOString()\n }\n }];\n}"
},
"typeVersion": 2
},
{
"id": "62b7db30-b6f2-4608-bcd8-f866759bcf56",
"name": "读取Excel文件",
"type": "n8n-nodes-base.readBinaryFile",
"position": [
-288,
496
],
"parameters": {
"filePath": "={{ $json.path_to_file }}"
},
"typeVersion": 1
},
{
"id": "40d06cd0-e57e-4d9d-8557-a06f64126d43",
"name": "解析Excel",
"type": "n8n-nodes-base.spreadsheetFile",
"position": [
-96,
496
],
"parameters": {
"options": {
"headerRow": true,
"sheetName": "={{ $node['Set Parameters'].json.sheet_name }}",
"includeEmptyCells": false
},
"fileFormat": "xlsx"
},
"typeVersion": 2
},
{
"id": "5f385292-ebab-4c4f-92b9-15c7ee875e02",
"name": "设置 - 定义文件路径",
"type": "n8n-nodes-base.set",
"position": [
-272,
-64
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "9cbd4ec9-df24-41e8-b47a-720a4cdb733b",
"name": "path_to_converter",
"type": "string",
"value": "C:\\Users\\Artem Boiko\\Desktop\\n8n pipelines\\DDC_Converter_Revit\\datadrivenlibs\\RvtExporter.exe"
},
{
"id": "aa834467-80fb-476a-bac1-6728478834f0",
"name": "project_file",
"type": "string",
"value": "C:\\Users\\Artem Boiko\\Documents\\GitHub\\cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto\\Sample_Projects\\2023 racbasicsampleproject.rvt"
},
{
"id": "4e4f5e6f-7a8b-4c5d-9e0f-1a2b3c4d5e6f",
"name": "group_by",
"type": "string",
"value": "Type Name"
},
{
"id": "5f6a7b8c-9d0e-4f1a-2b3c-4d5e6f7a8b9c",
"name": "country",
"type": "string",
"value": "Germany"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "c8ae79e4-3a12-4b9e-bcc2-129adf5ecf25",
"name": "创建 - Excel文件名",
"type": "n8n-nodes-base.set",
"position": [
-48,
-64
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "xlsx-filename-id",
"name": "xlsx_filename",
"type": "string",
"value": "={{ $json[\"project_file\"].slice(0, -4) + \"_rvt.xlsx\" }}"
},
{
"id": "path-to-converter-pass",
"name": "path_to_converter",
"type": "string",
"value": "={{ $json[\"path_to_converter\"] }}"
},
{
"id": "project-file-pass",
"name": "project_file",
"type": "string",
"value": "={{ $json[\"project_file\"] }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "60bcb5b5-2f61-4314-94a2-7d0bf53427fe",
"name": "检查 - Excel文件是否存在?",
"type": "n8n-nodes-base.readBinaryFile",
"position": [
144,
-64
],
"parameters": {
"filePath": "={{ $json[\"xlsx_filename\"] }}"
},
"typeVersion": 1,
"continueOnFail": true,
"alwaysOutputData": true
},
{
"id": "4a82e1e2-4c87-4132-82f0-bfcacf07ca38",
"name": "如果 - 文件存在?",
"type": "n8n-nodes-base.if",
"position": [
304,
-64
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "e7fb1577-e753-43f5-9f5a-4d5285aeb96e",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $binary.data ? true : false }}",
"rightValue": "={{ true }}"
}
]
}
},
"typeVersion": 2
},
{
"id": "3b60c81d-ec57-4183-b6e0-03fdf6636739",
"name": "提取 - 运行转换器",
"type": "n8n-nodes-base.executeCommand",
"position": [
64,
144
],
"parameters": {
"command": "=\"{{$node[\"Setup - Define file paths\"].json[\"path_to_converter\"]}}\" \"{{$node[\"Setup - Define file paths\"].json[\"project_file\"]}}\""
},
"typeVersion": 1,
"continueOnFail": true
},
{
"id": "4bf02c30-f53f-4345-a564-9f6b8f7f8a14",
"name": "信息 - 跳过转换",
"type": "n8n-nodes-base.set",
"position": [
496,
-80
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "status-id",
"name": "status",
"type": "string",
"value": "File already exists - skipping conversion"
},
{
"id": "xlsx-filename-id",
"name": "xlsx_filename",
"type": "string",
"value": "={{ $node[\"Create - Excel filename\"].json[\"xlsx_filename\"] }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "3b014ecd-d104-4d18-8d78-260e45b9b61a",
"name": "检查 - 提取是否成功?",
"type": "n8n-nodes-base.if",
"position": [
272,
144
],
"parameters": {
"options": {},
"conditions": {
"options": {
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "condition1",
"operator": {
"type": "object",
"operation": "exists",
"rightType": "any"
},
"leftValue": "={{ $node[\"Extract - Run converter\"].json.error }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2
},
{
"id": "760e0a5b-86a1-4515-b5bb-58a88448eba5",
"name": "错误 - 显示问题详情",
"type": "n8n-nodes-base.set",
"position": [
496,
80
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "error-message-id",
"name": "error_message",
"type": "string",
"value": "=Extraction failed: {{ $node[\"Extract - Run converter\"].json.error || \"Unknown error\" }}"
},
{
"id": "error-code-id",
"name": "error_code",
"type": "number",
"value": "={{ $node[\"Extract - Run converter\"].json.code || -1 }}"
},
{
"id": "xlsx-filename-error",
"name": "xlsx_filename",
"type": "string",
"value": "={{ $node[\"Create - Excel filename\"].json[\"xlsx_filename\"] }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "977dcd47-8b05-4d18-9fa8-e8ce86ca6f8a",
"name": "成功后设置xlsx_filename",
"type": "n8n-nodes-base.set",
"position": [
496,
256
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "xlsx-filename-success",
"name": "xlsx_filename",
"type": "string",
"value": "={{ $node[\"Create - Excel filename\"].json[\"xlsx_filename\"] }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "04eb1693-ca61-4ec5-ad69-7993df0cb087",
"name": "合并 - 继续工作流",
"type": "n8n-nodes-base.merge",
"position": [
688,
-16
],
"parameters": {},
"typeVersion": 3
},
{
"id": "bc8164ab-2ae9-4e1d-b180-d492b1296473",
"name": "设置参数",
"type": "n8n-nodes-base.set",
"position": [
832,
256
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "path-id",
"name": "path_to_file",
"type": "string",
"value": "={{ $json.xlsx_filename }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "28c70099-5314-4475-8ee0-0435a1e5ece8",
"name": "当点击“执行工作流”时",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-480,
-64
],
"parameters": {},
"typeVersion": 1
},
{
"id": "b9899d95-37cf-4540-902b-dce17b8f90ec",
"name": "转换块",
"type": "n8n-nodes-base.stickyNote",
"position": [
-864,
-240
],
"parameters": {
"color": 5,
"width": 1912,
"height": 660,
"content": "## 🔄 转换块"
},
"typeVersion": 1
},
{
"id": "8e7df7b7-d4ab-499a-aacb-3e358e34da19",
"name": "数据加载块",
"type": "n8n-nodes-base.stickyNote",
"position": [
-864,
448
],
"parameters": {
"color": 6,
"width": 1920,
"height": 240,
"content": "## 📊 块1:数据加载和处理"
},
"typeVersion": 1
},
{
"id": "26af2822-394b-4746-bf11-7bd7c6a692c1",
"name": "元素分类块",
"type": "n8n-nodes-base.stickyNote",
"position": [
-864,
720
],
"parameters": {
"color": 6,
"width": 1920,
"height": 256,
"content": "## 🏗️ 块2:元素分类"
},
"typeVersion": 1
},
{
"id": "2b8e5d57-8f51-41d4-8558-594d921e2311",
"name": "材料分析块",
"type": "n8n-nodes-base.stickyNote",
"position": [
-864,
1008
],
"parameters": {
"color": 5,
"width": 1920,
"height": 448,
"content": ""
},
"typeVersion": 1
},
{
"id": "ebc11ab6-d7db-4a1a-a46d-0938321d3c88",
"name": "CO2计算和报告块",
"type": "n8n-nodes-base.stickyNote",
"position": [
-864,
1488
],
"parameters": {
"color": 5,
"width": 1920,
"height": 416,
"content": "## 🌍 块4:CO2计算和报告"
},
"typeVersion": 1
},
{
"id": "bcb4b571-4db5-4d91-b057-cf3f91f1c16b",
"name": "设置说明",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1216,
-240
],
"parameters": {
"width": 336,
"height": 384,
"content": "## 📝 设置说明"
},
"typeVersion": 1
},
{
"id": "3abb7e6c-6004-4da0-ba89-f36d893c495f",
"name": "重要说明",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1216,
160
],
"parameters": {
"width": 336,
"height": 288,
"content": "## ⚠️ 重要信息"
},
"typeVersion": 1
},
{
"id": "8d7c13c3-9044-45bc-9759-9baf1d175aa1",
"name": "准备HTML路径",
"type": "n8n-nodes-base.code",
"position": [
528,
1552
],
"parameters": {
"jsCode": "// Get the project file path from the original setup\nconst projectFile = $node['Setup - Define file paths'].json.project_file;\nconst htmlContent = $node['HTML to Binary'].json.html || $input.first().binary.data;\n\n// Extract directory path from project file\nconst path = projectFile.substring(0, projectFile.lastIndexOf('\\\\'));\n\n// Create filename with timestamp\nconst timestamp = new Date().toISOString().slice(0,10);\nconst htmlFileName = `CO2_Analysis_Report_${timestamp}.html`;\nconst fullPath = `${path}\\\\${htmlFileName}`;\n\nconsole.log('Project file:', projectFile);\nconsole.log('Directory:', path);\nconsole.log('HTML file will be saved to:', fullPath);\n\nreturn [{\n json: {\n html_filename: htmlFileName,\n full_path: fullPath,\n directory: path,\n project_file: projectFile\n },\n binary: $input.first().binary\n}];"
},
"typeVersion": 2
},
{
"id": "e4851576-a8ff-4618-ba9c-cae2ab579d5c",
"name": "将HTML写入项目文件夹",
"type": "n8n-nodes-base.writeBinaryFile",
"position": [
704,
1552
],
"parameters": {
"options": {},
"fileName": "={{ $json.full_path }}"
},
"typeVersion": 1
},
{
"id": "7bd64a70-7bc3-47d3-9db8-c766aec135d6",
"name": "在浏览器中打开HTML",
"type": "n8n-nodes-base.executeCommand",
"position": [
880,
1552
],
"parameters": {
"command": "=start \"\" \"{{ $json.full_path }}\""
},
"typeVersion": 1
},
{
"id": "165b21cb-90a7-43e7-870c-036813bb60b6",
"name": "准备Excel路径",
"type": "n8n-nodes-base.code",
"position": [
704,
1712
],
"parameters": {
"jsCode": "// Similar logic for Excel file\nconst projectFile = $node['Setup - Define file paths'].json.project_file;\n\n// Extract directory path from project file\nconst path = projectFile.substring(0, projectFile.lastIndexOf('\\\\'));\n\n// Create filename with timestamp\nconst timestamp = new Date().toISOString().slice(0,10);\nconst excelFileName = `CO2_Analysis_Professional_Report_${timestamp}.xlsx`;\nconst fullPath = `${path}\\\\${excelFileName}`;\n\nconsole.log('Excel file will be saved to:', fullPath);\n\nreturn [{\n json: {\n excel_filename: excelFileName,\n full_path: fullPath,\n directory: path,\n project_file: projectFile\n },\n binary: $input.first().binary\n}];"
},
"typeVersion": 2
},
{
"id": "e1e918c3-c5a3-4382-878a-5cdf19c1fd48",
"name": "将Excel写入项目文件夹",
"type": "n8n-nodes-base.writeBinaryFile",
"position": [
880,
1712
],
"parameters": {
"options": {},
"fileName": "={{ $json.full_path }}"
},
"typeVersion": 1
},
{
"id": "319e5fdf-1b31-48c6-9a8f-006c03fc23dd",
"name": "生成HTML报告",
"type": "n8n-nodes-base.code",
"position": [
192,
1552
],
"parameters": {
"jsCode": "// Generate professional McKinsey/Accenture style HTML report with charts\nconst items = $input.all();\nconst projectTotals = $getWorkflowStaticData('global').projectTotals;\n\n// Get project name from the setup node\nconst projectFilePath = $node['Setup - Define file paths'].json.project_file || '';\nconst projectFileName = projectFilePath.split('\\\\').pop().split('/').pop().replace(/\\.[^/.]+$/, '');\n\n// Calculate key metrics\nconst topMaterial = Object.entries(projectTotals.byMaterial)\n .sort((a, b) => b[1].co2 - a[1].co2)[0];\nconst highImpactItems = items.filter(item => \n parseFloat(item.json['Total CO2 (tonnes CO2e)']) >= projectTotals.totalCO2 * 0.05\n).length;\n\nconst html = `<!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>Carbon Footprint Analysis | ${projectFileName}</title>\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.9.1/chart.min.js\"></script>\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n \n body {\n font-family: 'Arial', 'Helvetica Neue', sans-serif;\n background: #ffffff;\n color: #2e2e38;\n line-height: 1.6;\n font-size: 13px;\n }\n \n .container {\n max-width: 1100px;\n margin: 0 auto;\n padding: 40px 20px;\n }\n \n /* Header - McKinsey style */\n .header {\n border-bottom: 3px solid #0061a0;\n padding-bottom: 20px;\n margin-bottom: 30px;\n }\n \n .header h1 {\n font-size: 32px;\n font-weight: 300;\n color: #0061a0;\n margin-bottom: 8px;\n letter-spacing: -0.5px;\n }\n \n .header .subtitle {\n color: #696969;\n font-size: 16px;\n font-weight: 400;\n }\n \n /* Executive Summary - Accenture purple accent */\n .executive-summary {\n background: linear-gradient(to right, #460073 0%, #0061a0 100%);\n color: white;\n padding: 30px;\n margin-bottom: 30px;\n position: relative;\n }\n \n .executive-summary h2 {\n font-size: 20px;\n font-weight: 400;\n margin-bottom: 15px;\n text-transform: uppercase;\n letter-spacing: 1px;\n }\n \n .executive-summary p {\n font-size: 15px;\n line-height: 1.8;\n opacity: 0.95;\n }\n \n /* KPI Cards - McKinsey teal */\n .kpi-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));\n gap: 20px;\n margin-bottom: 30px;\n }\n \n .kpi-card {\n background: #ffffff;\n border: 1px solid #e0e0e0;\n padding: 25px;\n position: relative;\n transition: all 0.3s ease;\n }\n \n .kpi-card:hover {\n box-shadow: 0 4px 12px rgba(0,0,0,0.1);\n transform: translateY(-2px);\n }\n \n .kpi-card.primary {\n border-top: 4px solid #00a19a;\n }\n \n .kpi-card.secondary {\n border-top: 4px solid #460073;\n }\n \n .kpi-value {\n font-size: 36px;\n font-weight: 300;\n color: #0061a0;\n margin-bottom: 8px;\n line-height: 1;\n }\n \n .kpi-label {\n font-size: 12px;\n color: #696969;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n font-weight: 600;\n }\n \n .kpi-change {\n position: absolute;\n top: 20px;\n right: 20px;\n font-size: 11px;\n color: #00a19a;\n font-weight: 600;\n }\n \n /* Chart Containers */\n .charts-grid {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 30px;\n margin-bottom: 30px;\n }\n \n .chart-container {\n background: #ffffff;\n border: 1px solid #e0e0e0;\n padding: 20px;\n position: relative;\n }\n \n .chart-container.full-width {\n grid-column: 1 / -1;\n }\n \n .chart-header {\n margin-bottom: 20px;\n padding-bottom: 10px;\n border-bottom: 1px solid #f0f0f0;\n }\n \n .chart-header h3 {\n font-size: 16px;\n font-weight: 600;\n color: #2e2e38;\n margin: 0;\n }\n \n .chart-header p {\n font-size: 12px;\n color: #696969;\n margin-top: 5px;\n }\n \n .chart-wrapper {\n position: relative;\n height: 300px;\n }\n \n .chart-wrapper.small {\n height: 250px;\n }\n \n /* Insights Section - BCG green */\n .insight-section {\n background: #f8f8f8;\n border-left: 5px solid #009a44;\n padding: 25px 30px;\n margin-bottom: 30px;\n }\n \n .insight-section h3 {\n color: #009a44;\n font-size: 18px;\n font-weight: 600;\n margin-bottom: 12px;\n }\n \n .insight-section p {\n color: #2e2e38;\n font-size: 14px;\n line-height: 1.8;\n }\n \n .insight-highlight {\n color: #0061a0;\n font-weight: 600;\n }\n \n /* Data Tables - Professional style */\n .table-container {\n background: #ffffff;\n border: 1px solid #e0e0e0;\n margin-bottom: 30px;\n overflow: hidden;\n }\n \n .table-header {\n background: #f5f5f5;\n padding: 15px 20px;\n border-bottom: 1px solid #e0e0e0;\n }\n \n .table-header h3 {\n font-size: 16px;\n font-weight: 600;\n color: #2e2e38;\n margin: 0;\n }\n \n table {\n width: 100%;\n border-collapse: collapse;\n font-size: 13px;\n }\n \n th {\n background: #fafafa;\n padding: 12px 15px;\n text-align: left;\n font-weight: 600;\n color: #2e2e38;\n border-bottom: 2px solid #e0e0e0;\n font-size: 12px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n \n td {\n padding: 12px 15px;\n border-bottom: 1px solid #f0f0f0;\n color: #4a4a4a;\n }\n \n tr:hover {\n background: #f9f9f9;\n }\n \n .value-highlight {\n font-weight: 600;\n color: #0061a0;\n }\n \n /* Impact levels - Traffic light system */\n .impact-critical {\n display: inline-block;\n padding: 3px 8px;\n background: #dc3545;\n color: white;\n font-size: 11px;\n font-weight: 600;\n border-radius: 2px;\n }\n \n .impact-high {\n display: inline-block;\n padding: 3px 8px;\n background: #ff7043;\n color: white;\n font-size: 11px;\n font-weight: 600;\n border-radius: 2px;\n }\n \n .impact-medium {\n display: inline-block;\n padding: 3px 8px;\n background: #ffa726;\n color: white;\n font-size: 11px;\n font-weight: 600;\n border-radius: 2px;\n }\n \n .impact-low {\n display: inline-block;\n padding: 3px 8px;\n background: #66bb6a;\n color: white;\n font-size: 11px;\n font-weight: 600;\n border-radius: 2px;\n }\n \n /* Progress bars */\n .progress-bar {\n background: #e0e0e0;\n height: 6px;\n border-radius: 3px;\n overflow: hidden;\n width: 100px;\n display: inline-block;\n vertical-align: middle;\n margin-left: 10px;\n }\n \n .progress-fill {\n height: 100%;\n background: #00a19a;\n transition: width 0.3s ease;\n }\n \n /* Action items - Professional blue-grey */\n .action-box {\n background: #f8f9fa;\n border-left: 4px solid #0061a0;\n padding: 25px 30px;\n margin: 30px 0;\n }\n \n .action-box h4 {\n color: #2e2e38;\n font-size: 16px;\n font-weight: 600;\n margin-bottom: 15px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n \n .action-list {\n list-style: none;\n padding: 0;\n }\n \n .action-list li {\n padding: 10px 0;\n padding-left: 30px;\n position: relative;\n color: #4a4a4a;\n line-height: 1.6;\n }\n \n .action-list li:before {\n content: \"→\";\n position: absolute;\n left: 0;\n color: #0061a0;\n font-weight: bold;\n }\n \n /* Footer */\n .footer {\n margin-top: 50px;\n padding-top: 20px;\n border-top: 1px solid #e0e0e0;\n text-align: center;\n color: #696969;\n font-size: 11px;\n }\n \n .footer .logo {\n font-weight: 600;\n color: #0061a0;\n }\n \n /* Print optimization */\n @media print {\n body { background: white; }\n .container { padding: 20px; }\n .table-container { box-shadow: none; border: 1px solid #ddd; }\n .kpi-card { box-shadow: none; }\n }\n \n /* Responsive */\n @media (max-width: 768px) {\n .kpi-grid { grid-template-columns: 1fr; }\n .charts-grid { grid-template-columns: 1fr; }\n .header h1 { font-size: 24px; }\n .kpi-value { font-size: 28px; }\n }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <!-- Header -->\n <div class=\"header\">\n <h1>Carbon Footprint Analysis</h1>\n <div class=\"subtitle\">Project: <strong>${projectFileName}</strong> | Executive Summary Report | ${new Date().toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}</div>\n </div>\n \n <!-- Executive Summary -->\n <div class=\"executive-summary\">\n <h2>Executive Summary</h2>\n <p>\n Analysis reveals <strong>${projectTotals.totalCO2.toFixed(1)} tonnes CO2e</strong> of embodied carbon across \n ${projectTotals.totalElements.toLocaleString()} building elements. The assessment identifies ${highImpactItems} \n high-impact element groups contributing over 5% each to total emissions, presenting clear optimization opportunities.\n </p>\n </div>\n \n <!-- KPI Cards -->\n <div class=\"kpi-grid\">\n <div class=\"kpi-card primary\">\n <div class=\"kpi-value\">${projectTotals.totalCO2.toFixed(0)}</div>\n <div class=\"kpi-label\">Total CO2 Emissions</div>\n <div class=\"kpi-change\">tonnes CO2e</div>\n </div>\n <div class=\"kpi-card secondary\">\n <div class=\"kpi-value\">${((topMaterial[1].co2 / projectTotals.totalCO2) * 100).toFixed(0)}%</div>\n <div class=\"kpi-label\">${topMaterial[0]} Impact</div>\n <div class=\"kpi-change\">of total</div>\n </div>\n <div class=\"kpi-card primary\">\n <div class=\"kpi-value\">${(projectTotals.totalCO2 / projectTotals.totalMass).toFixed(1)}</div>\n <div class=\"kpi-label\">Average Intensity</div>\n <div class=\"kpi-change\">kg CO2e/kg</div>\n </div>\n <div class=\"kpi-card secondary\">\n <div class=\"kpi-value\">${Object.keys(projectTotals.byMaterial).length}</div>\n <div class=\"kpi-label\">Material Types</div>\n <div class=\"kpi-change\">identified</div>\n </div>\n </div>\n \n <!-- Charts Section -->\n <div class=\"charts-grid\">\n <!-- Pie Chart - Material Distribution -->\n <div class=\"chart-container\">\n <div class=\"chart-header\">\n <h3>CO2 Emissions by Material Type</h3>\n <p>Proportional distribution of carbon impact</p>\n </div>\n <div class=\"chart-wrapper small\">\n <canvas id=\"materialPieChart\"></canvas>\n </div>\n </div>\n \n <!-- Donut Chart - Impact Concentration -->\n <div class=\"chart-container\">\n <div class=\"chart-header\">\n <h3>Impact Concentration Analysis</h3>\n <p>Top 5 vs. remaining elements</p>\n </div>\n <div class=\"chart-wrapper small\">\n <canvas id=\"concentrationChart\"></canvas>\n </div>\n </div>\n \n <!-- Bar Chart - Top Contributors -->\n <div class=\"chart-container full-width\">\n <div class=\"chart-header\">\n <h3>Top 10 Carbon Contributors</h3>\n <p>Individual element groups ranked by CO2 emissions</p>\n </div>\n <div class=\"chart-wrapper\">\n <canvas id=\"topContributorsBar\"></canvas>\n </div>\n </div>\n </div>\n \n <!-- Key Insight -->\n <div class=\"insight-section\">\n <h3>Primary Finding</h3>\n <p>\n <span class=\"insight-highlight\">${topMaterial[0]}</span> represents the largest carbon impact at \n <span class=\"insight-highlight\">${topMaterial[1].co2.toFixed(1)} tonnes CO2e</span>, accounting for \n <span class=\"insight-highlight\">${((topMaterial[1].co2 / projectTotals.totalCO2) * 100).toFixed(0)}%</span> \n of total emissions across ${topMaterial[1].elements.toLocaleString()} elements. This concentration presents \n the primary opportunity for carbon reduction through material substitution or design optimization.\n </p>\n </div>\n \n <!-- Top Contributors Table -->\n <div class=\"table-container\">\n <div class=\"table-header\">\n <h3>Top 10 Carbon Contributors</h3>\n </div>\n <table>\n <thead>\n <tr>\n <th style=\"width: 60px;\">Rank</th>\n <th>Element Group</th>\n <th>Material Type</th>\n <th style=\"width: 80px;\">Quantity</th>\n <th style=\"width: 100px;\">CO2 (tonnes)</th>\n <th style=\"width: 150px;\">% of Total</th>\n <th style=\"width: 80px;\">Priority</th>\n </tr>\n </thead>\n <tbody>\n ${items.slice(0, 10).map((item, index) => {\n const data = item.json;\n const co2Percent = parseFloat(data['CO2 % of Total']);\n let impactClass = 'impact-low';\n let impactText = 'LOW';\n \n if (co2Percent >= 10) {\n impactClass = 'impact-critical';\n impactText = 'CRITICAL';\n } else if (co2Percent >= 5) {\n impactClass = 'impact-high';\n impactText = 'HIGH';\n } else if (co2Percent >= 2) {\n impactClass = 'impact-medium';\n impactText = 'MEDIUM';\n }\n \n return `\n <tr>\n <td>${index + 1}</td>\n <td class=\"value-highlight\">${data['Element Name']}</td>\n <td>${data['Material (EU Standard)']}</td>\n <td>${data['Element Count']}</td>\n <td class=\"value-highlight\">${parseFloat(data['Total CO2 (tonnes CO2e)']).toFixed(2)}</td>\n <td>\n ${co2Percent.toFixed(1)}%\n <div class=\"progress-bar\">\n <div class=\"progress-fill\" style=\"width: ${Math.min(co2Percent * 5, 100)}%\"></div>\n </div>\n </td>\n <td><span class=\"${impactClass}\">${impactText}</span></td>\n </tr>`;\n }).join('')}\n </tbody>\n </table>\n </div>\n \n <!-- Material Analysis Table -->\n <div class=\"table-container\">\n <div class=\"table-header\">\n <h3>Material Impact Analysis</h3>\n </div>\n <table>\n <thead>\n <tr>\n <th>Material Classification</th>\n <th style=\"width: 100px;\">Elements</th>\n <th style=\"width: 100px;\">Mass (t)</th>\n <th style=\"width: 100px;\">CO2 (t)</th>\n <th style=\"width: 150px;\">% of Total</th>\n <th style=\"width: 120px;\">Intensity</th>\n </tr>\n </thead>\n <tbody>\n ${Object.entries(projectTotals.byMaterial)\n .sort((a, b) => b[1].co2 - a[1].co2)\n .slice(0, 6)\n .map(([material, data]) => {\n const percent = (data.co2 / projectTotals.totalCO2) * 100;\n return `\n <tr>\n <td class=\"value-highlight\">${material}</td>\n <td>${data.elements.toLocaleString()}</td>\n <td>${data.mass.toFixed(1)}</td>\n <td class=\"value-highlight\">${data.co2.toFixed(1)}</td>\n <td>\n ${percent.toFixed(1)}%\n <div class=\"progress-bar\">\n <div class=\"progress-fill\" style=\"width: ${Math.min(percent * 3, 100)}%\"></div>\n </div>\n </td>\n <td>${(data.co2 / data.mass).toFixed(2)} kg/kg</td>\n </tr>`;\n }).join('')}\n </tbody>\n </table>\n </div>\n \n <!-- Cumulative Impact Chart - moved here after Material Analysis -->\n <div class=\"charts-grid\">\n <div class=\"chart-container full-width\">\n <div class=\"chart-header\">\n <h3>Cumulative Carbon Impact</h3>\n <p>Pareto analysis showing concentration of emissions</p>\n </div>\n <div class=\"chart-wrapper\">\n <canvas id=\"cumulativeChart\"></canvas>\n </div>\n </div>\n </div>\n \n <!-- Action Items -->\n <div class=\"action-box\">\n <h4>Recommended Actions</h4>\n <ul class=\"action-list\">\n <li>Prioritize ${topMaterial[0]} optimization - potential ${(topMaterial[1].co2 * 0.2).toFixed(0)}t CO2e reduction with 20% improvement</li>\n <li>Review specification for ${highImpactItems} high-impact element groups (>5% each of total)</li>\n <li>Investigate low-carbon alternatives for top 3 materials: ${Object.entries(projectTotals.byMaterial).sort((a,b) => b[1].co2 - a[1].co2).slice(0,3).map(([m]) => m).join(', ')}</li>\n <li>Focus on elements exceeding project average intensity of ${(projectTotals.totalCO2 / projectTotals.totalMass).toFixed(1)} kg CO2e per kg material</li>\n </ul>\n </div>\n \n <!-- Footer -->\n <div class=\"footer\">\n <p>\n <span class=\"logo\">Carbon Footprint Analysis</span> • \n Embodied Carbon Assessment (A1-A3) • \n Generated ${new Date().toLocaleString()}\n </p>\n <p style=\"margin-top: 8px;\">\n Powered by DataDrivenConstruction.io • \n Detailed calculations available in accompanying Excel workbook\n </p>\n </div>\n </div>\n \n <script>\n // Chart.js global configuration\n Chart.defaults.font.family = \"'Arial', 'Helvetica Neue', sans-serif\";\n Chart.defaults.font.size = 11;\n \n // Professional color palette\n const colors = {\n primary: ['#0061a0', '#00a19a', '#460073', '#009a44', '#ff7043', '#ffa726'],\n secondary: ['#4fc3f7', '#81c784', '#ba68c8', '#ffb74d', '#e57373', '#64b5f6'],\n gradient: ['rgba(0, 97, 160, 0.8)', 'rgba(0, 161, 154, 0.8)', 'rgba(70, 0, 115, 0.8)']\n };\n \n // Material Pie Chart\n const materialData = ${JSON.stringify(Object.entries(projectTotals.byMaterial)\n .sort((a, b) => b[1].co2 - a[1].co2)\n .slice(0, 6)\n .map(([material, data]) => ({\n label: material,\n value: data.co2\n })))};\n \n new Chart(document.getElementById('materialPieChart'), {\n type: 'pie',\n data: {\n labels: materialData.map(d => d.label),\n datasets: [{\n data: materialData.map(d => d.value.toFixed(1)),\n backgroundColor: colors.primary,\n borderColor: '#ffffff',\n borderWidth: 2\n }]\n },\n options: {\n responsive: true,\n maintainAspectRatio: false,\n plugins: {\n legend: {\n position: 'right',\n labels: {\n padding: 10,\n usePointStyle: true,\n font: { size: 11 }\n }\n },\n tooltip: {\n callbacks: {\n label: function(context) {\n const total = context.dataset.data.reduce((a, b) => parseFloat(a) + parseFloat(b), 0);\n const percent = ((context.parsed / total) * 100).toFixed(1);\n return context.label + ': ' + context.parsed + 't (' + percent + '%)';\n }\n }\n }\n }\n }\n });\n \n // Concentration Donut Chart\n const top5Total = ${items.slice(0, 5).reduce((sum, item) => \n sum + parseFloat(item.json['Total CO2 (tonnes CO2e)']), 0).toFixed(1)};\n const remainingTotal = ${(projectTotals.totalCO2 - items.slice(0, 5).reduce((sum, item) => \n sum + parseFloat(item.json['Total CO2 (tonnes CO2e)']), 0)).toFixed(1)};\n \n new Chart(document.getElementById('concentrationChart'), {\n type: 'doughnut',\n data: {\n labels: ['Top 5 Elements', 'Remaining Elements'],\n datasets: [{\n data: [top5Total, remainingTotal],\n backgroundColor: ['#0061a0', '#e0e0e0'],\n borderColor: '#ffffff',\n borderWidth: 2\n }]\n },\n options: {\n responsive: true,\n maintainAspectRatio: false,\n cutout: '60%',\n plugins: {\n legend: {\n position: 'bottom',\n labels: {\n padding: 15,\n usePointStyle: true,\n font: { size: 11 }\n }\n },\n tooltip: {\n callbacks: {\n label: function(context) {\n const percent = ((context.parsed / ${projectTotals.totalCO2}) * 100).toFixed(1);\n return context.label + ': ' + context.parsed + 't (' + percent + '%)';\n }\n }\n }\n }\n }\n });\n \n // Top Contributors Bar Chart\n const topItems = ${JSON.stringify(items.slice(0, 10).map(item => ({\n name: item.json['Element Name'],\n co2: parseFloat(item.json['Total CO2 (tonnes CO2e)'])\n })))};\n \n new Chart(document.getElementById('topContributorsBar'), {\n type: 'bar',\n data: {\n labels: topItems.map(d => d.name),\n datasets: [{\n label: 'CO2 Emissions (tonnes)',\n data: topItems.map(d => d.co2),\n backgroundColor: topItems.map((d, i) => {\n const percent = (d.co2 / ${projectTotals.totalCO2}) * 100;\n if (percent >= 10) return '#dc3545';\n if (percent >= 5) return '#ff7043';\n if (percent >= 2) return '#ffa726';\n return '#66bb6a';\n }),\n borderColor: 'rgba(0, 0, 0, 0.1)',\n borderWidth: 1\n }]\n },\n options: {\n responsive: true,\n maintainAspectRatio: false,\n indexAxis: 'y',\n plugins: {\n legend: {\n display: false\n },\n tooltip: {\n callbacks: {\n afterLabel: function(context) {\n const percent = ((context.parsed.x / ${projectTotals.totalCO2}) * 100).toFixed(1);\n return percent + '% of total';\n }\n }\n }\n },\n scales: {\n x: {\n grid: {\n color: 'rgba(0, 0, 0, 0.05)'\n },\n ticks: {\n callback: function(value) {\n return value + 't';\n }\n }\n },\n y: {\n grid: {\n display: false\n },\n ticks: {\n font: {\n size: 10\n }\n }\n }\n }\n }\n });\n \n // Cumulative Impact Area Chart\n const allItems = ${JSON.stringify(items.map(item => ({\n name: item.json['Element Name'],\n co2: parseFloat(item.json['Total CO2 (tonnes CO2e)'])\n })))};\n \n let cumulative = 0;\n const cumulativeData = allItems.map((item, index) => {\n cumulative += item.co2;\n return {\n x: index + 1,\n y: cumulative,\n percent: (cumulative / ${projectTotals.totalCO2}) * 100\n };\n });\n \n new Chart(document.getElementById('cumulativeChart'), {\n type: 'line',\n data: {\n labels: cumulativeData.map(d => 'Element ' + d.x),\n datasets: [{\n label: 'Cumulative CO2',\n data: cumulativeData.map(d => d.y),\n borderColor: '#0061a0',\n backgroundColor: 'rgba(0, 97, 160, 0.1)',\n fill: true,\n tension: 0.4,\n pointRadius: 0,\n pointHoverRadius: 4\n }, {\n label: 'Cumulative %',\n data: cumulativeData.map(d => d.percent),\n borderColor: '#00a19a',\n backgroundColor: 'transparent',\n yAxisID: 'y1',\n borderDash: [5, 5],\n fill: false,\n tension: 0.4,\n pointRadius: 0,\n pointHoverRadius: 4\n }]\n },\n options: {\n responsive: true,\n maintainAspectRatio: false,\n interaction: {\n mode: 'index',\n intersect: false\n },\n plugins: {\n legend: {\n position: 'top',\n labels: {\n usePointStyle: true,\n padding: 15\n }\n },\n tooltip: {\n callbacks: {\n title: function(context) {\n const index = context[0].dataIndex;\n if (index < allItems.length) {\n return allItems[index].name;\n }\n return 'Element ' + (index + 1);\n }\n }\n }\n },\n scales: {\n x: {\n display: false\n },\n y: {\n type: 'linear',\n display: true,\n position: 'left',\n grid: {\n color: 'rgba(0, 0, 0, 0.05)'\n },\n ticks: {\n callback: function(value) {\n return value.toFixed(0) + 't';\n }\n },\n title: {\n display: true,\n text: 'Cumulative CO2 (tonnes)'\n }\n },\n y1: {\n type: 'linear',\n display: true,\n position: 'right',\n grid: {\n drawOnChartArea: false\n },\n ticks: {\n callback: function(value) {\n return value.toFixed(0) + '%';\n }\n },\n title: {\n display: true,\n text: 'Cumulative Percentage'\n }\n }\n }\n }\n });\n </script>\n</body>\n</html>`;\n\nreturn [{\n json: {\n html: html,\n reportType: 'mckinsey-accenture-professional',\n timestamp: new Date().toISOString()\n }\n}];"
},
"typeVersion": 2
},
{
"id": "8d2144e0-ea60-46c3-ab58-1f2e3a988d2d",
"name": "HTML 转二进制",
"type": "n8n-nodes-base.code",
"position": [
352,
1552
],
"parameters": {
"jsCode": "\n// Convert HTML to binary for file output\nconst html = $input.first().json.html;\nconst fileName = `CO2_Analysis_Report_${new Date().toISOString().slice(0,10)}.html`;\n\n// Return with 'data' as the default binary field name\nreturn [{\n json: {\n fileName: fileName // Also pass filename in json for backup\n },\n binary: {\n data: {\n data: Buffer.from(html).toString('base64'),\n mimeType: 'text/html',\n fileName: fileName,\n fileExtension: 'html'\n }\n }\n}];"
},
"typeVersion": 2
},
{
"id": "b3a16d39-4c38-4c35-b810-1eeb3325694f",
"name": "便签",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1216,
464
],
"parameters": {
"width": 340,
"height": 116,
"content": "⭐ **如果您觉得我们的工具有用**,请考虑在[GitHub](https://github.com/datadrivenconstruction/cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto)上**给我们的仓库加星**。"
},
"typeVersion": 1
},
{
"id": "3089d1b0-7633-44c4-81ec-16b06cd61369",
"name": "转换块1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-864,
-352
],
"parameters": {
"color": 5,
"width": 1064,
"height": 96,
"content": "# 适用于Revit和IFC的AI(LLM)碳足迹CO2估算器"
},
"typeVersion": 1
},
{
"id": "6cdf5726-dbbc-420b-9feb-c00a007e2a25",
"name": "OpenAI 聊天模型",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
304,
1264
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-3.5-turbo",
"cachedResultName": "gpt-3.5-turbo"
},
"options": {}
},
"credentials": {
"openAiApi": {
"id": "5SwKOx6OOukR6C0w",
"name": "OpenAi account n8n"
}
},
"typeVersion": 1.2
},
{
"id": "f16d5db4-57a8-49fa-b678-d69b45e15d9a",
"name": "xAI Grok聊天模型",
"type": "@n8n/n8n-nodes-langchain.lmChatXAiGrok",
"position": [
480,
1264
],
"parameters": {
"model": "grok-4-0709",
"options": {}
},
"credentials": {
"xAiApi": {
"id": "JKhw9fFrSig9QNQB",
"name": "xAi account"
}
},
"typeVersion": 1
},
{
"id": "ff1a82e6-2138-45f3-a51e-4ce140f718e1",
"name": "便签2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-336,
-208
],
"parameters": {
"color": 4,
"height": 384,
"content": "## ⬇️ 仅在此处修改变量"
},
"typeVersion": 1
},
{
"id": "91417501-7993-4c62-bb22-095b30ce3c1a",
"name": "在标准3D视图上",
"type": "n8n-nodes-base.if",
"position": [
-256,
768
],
"parameters": {
"conditions": {
"boolean": [
{
"value1": "={{ $json['On the standard 3D View'] }}",
"value2": true
}
]
}
},
"typeVersion": 1
},
{
"id": "a607b5db-6257-4ba3-99b7-a31e5779e0ea",
"name": "非3D视图元素输出",
"type": "n8n-nodes-base.set",
"position": [
-80,
848
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "message",
"name": "message",
"type": "string",
"value": "Elements not visible in standard 3D view"
},
{
"id": "filtered_count",
"name": "filtered_count",
"type": "number",
"value": "={{ $input.all().length }}"
},
{
"id": "reason",
"name": "reason",
"type": "string",
"value": "Parameter 'On the standard 3D View' is not True"
}
]
}
},
"typeVersion": 3.4
}
],
"active": false,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "9380188d-a427-41be-9690-38a99e3a7a6b",
"connections": {
"Parse Excel": {
"main": [
[
{
"node": "Extract Headers and Data1",
"type": "main",
"index": 0
}
]
]
},
"HTML to Binary": {
"main": [
[
{
"node": "Prepare HTML Path",
"type": "main",
"index": 0
}
]
]
},
"Set Parameters": {
"main": [
[
{
"node": "Read Excel File",
"type": "main",
"index": 0
}
]
]
},
"Read Excel File": {
"main": [
[
{
"node": "Parse Excel",
"type": "main",
"index": 0
}
]
]
},
"AI Agent Enhanced": {
"main": [
[
{
"node": "Parse Enhanced Response",
"type": "main",
"index": 0
}
]
]
},
"Create Excel File": {
"main": [
[
{
"node": "Enhance Excel Output",
"type": "main",
"index": 0
}
]
]
},
"If - File exists?": {
"main": [
[
{
"node": "Info - Skip conversion",
"type": "main",
"index": 0
}
],
[
{
"node": "Extract - Run converter",
"type": "main",
"index": 0
}
]
]
},
"Prepare HTML Path": {
"main": [
[
{
"node": "Write HTML to Project Folder",
"type": "main",
"index": 0
}
]
]
},
"Accumulate Results": {
"main": [
[
{
"node": "Check If All Batches Done",
"type": "main",
"index": 0
}
]
]
},
"Prepare Excel Data": {
"main": [
[
{
"node": "Create Excel File",
"type": "main",
"index": 0
}
]
]
},
"Prepare Excel Path": {
"main": [
[
{
"node": "Write Excel to Project Folder",
"type": "main",
"index": 0
}
]
]
},
"Clean Empty Values1": {
"main": [
[
{
"node": "Prepare Enhanced Prompts",
"type": "main",
"index": 0
}
]
]
},
"Collect All Results": {
"main": [
[
{
"node": "Calculate Project Totals4",
"type": "main",
"index": 0
}
]
]
},
"Process in Batches1": {
"main": [
[
{
"node": "Clean Empty Values1",
"type": "main",
"index": 0
}
]
]
},
"xAI Grok Chat Model": {
"ai_languageModel": [
[
{
"node": "AI Agent Enhanced",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Enhance Excel Output": {
"main": [
[
{
"node": "Prepare Excel Path",
"type": "main",
"index": 0
}
]
]
},
"Generate HTML Report": {
"main": [
[
{
"node": "HTML to Binary",
"type": "main",
"index": 0
}
]
]
},
"Is Building Element1": {
"main": [
[
{
"node": "Process in Batches1",
"type": "main",
"index": 0
}
],
[
{
"node": "Non-Building Elements Output1",
"type": "main",
"index": 0
}
]
]
},
"Process AI Response1": {
"main": [
[
{
"node": "Group Data with AI Rules1",
"type": "main",
"index": 0
}
]
]
},
"Find Category Fields1": {
"main": [
[
{
"node": "AI Classify Categories1",
"type": "main",
"index": 0
}
]
]
},
"Info - Skip conversion": {
"main": [
[
{
"node": "Merge - Continue workflow",
"type": "main",
"index": 0
}
]
]
},
"AI Analyze All Headers1": {
"main": [
[
{
"node": "Process AI Response1",
"type": "main",
"index": 0
}
]
]
},
"AI Classify Categories1": {
"main": [
[
{
"node": "Apply Classification to Groups1",
"type": "main",
"index": 0
}
]
]
},
"Create - Excel filename": {
"main": [
[
{
"node": "Check - Does Excel file exist?",
"type": "main",
"index": 0
}
]
]
},
"Extract - Run converter": {
"main": [
[
{
"node": "Check - Did extraction succeed?",
"type": "main",
"index": 0
}
]
]
},
"On the standard 3D View": {
"main": [
[
{
"node": "Find Category Fields1",
"type": "main",
"index": 0
}
],
[
{
"node": "Non-3D View Elements Output",
"type": "main",
"index": 0
}
]
]
},
"Parse Enhanced Response": {
"main": [
[
{
"node": "Accumulate Results",
"type": "main",
"index": 0
}
]
]
},
"Prepare Enhanced Prompts": {
"main": [
[
{
"node": "AI Agent Enhanced",
"type": "main",
"index": 0
}
]
]
},
"Calculate Project Totals4": {
"main": [
[
{
"node": "Generate HTML Report",
"type": "main",
"index": 0
},
{
"node": "Prepare Excel Data",
"type": "main",
"index": 0
}
]
]
},
"Check If All Batches Done": {
"main": [
[
{
"node": "Collect All Results",
"type": "main",
"index": 0
}
],
[
{
"node": "Process in Batches1",
"type": "main",
"index": 0
}
]
]
},
"Extract Headers and Data1": {
"main": [
[
{
"node": "AI Analyze All Headers1",
"type": "main",
"index": 0
}
]
]
},
"Group Data with AI Rules1": {
"main": [
[
{
"node": "On the standard 3D View",
"type": "main",
"index": 0
}
]
]
},
"Merge - Continue workflow": {
"main": [
[
{
"node": "Set Parameters",
"type": "main",
"index": 0
}
]
]
},
"Setup - Define file paths": {
"main": [
[
{
"node": "Create - Excel filename",
"type": "main",
"index": 0
}
]
]
},
"Error - Show what went wrong": {
"main": [
[
{
"node": "Merge - Continue workflow",
"type": "main",
"index": 1
}
]
]
},
"Write HTML to Project Folder": {
"main": [
[
{
"node": "Open HTML in Browser",
"type": "main",
"index": 0
}
]
]
},
"Check - Does Excel file exist?": {
"main": [
[
{
"node": "If - File exists?",
"type": "main",
"index": 0
}
]
]
},
"Apply Classification to Groups1": {
"main": [
[
{
"node": "Is Building Element1",
"type": "main",
"index": 0
}
]
]
},
"Check - Did extraction succeed?": {
"main": [
[
{
"node": "Error - Show what went wrong",
"type": "main",
"index": 0
}
],
[
{
"node": "Set xlsx_filename after success",
"type": "main",
"index": 0
}
]
]
},
"Set xlsx_filename after success": {
"main": [
[
{
"node": "Merge - Continue workflow",
"type": "main",
"index": 1
}
]
]
},
"When clicking ‘Execute workflow’": {
"main": [
[
{
"node": "Setup - Define file paths",
"type": "main",
"index": 0
}
]
]
}
}
}常见问题
如何使用这个工作流?
复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。
这个工作流适合什么场景?
高级 - AI 摘要总结, 多模态 AI
需要付费吗?
本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。
相关工作流推荐
n8n_6_使用LLM为Revit和IFC进行施工造价估算
使用GPT-4和Claude基于Revit/IFC模型估算施工成本
If
Set
Code
+13
55 节点Artem Boiko
AI 摘要总结
n8n_3_CAD-BIM-批量转换器-管道
批量转换 CAD/BIM 文件为 XLSX/DAE,带验证和报告
If
Set
Code
+14
82 节点Artem Boiko
文档提取
在可视化参考库中探索n8n节点
在可视化参考库中探索n8n节点
If
Ftp
Set
+93
113 节点I versus AI
其他
完整的 B2B 销售流程:Apollo 潜在客户生成、Mailgun 外展和 AI 回复管理
完整的 B2B 销售流程:Apollo 潜在客户生成、Mailgun 外展和 AI 回复管理
If
Set
Code
+26
116 节点Paul
内容创作
我的工作流11
根据Excel标准验证CAD-BIM文件(Revit/IFC/DWG/DGN)
If
Set
Code
+6
26 节点Artem
工程
YNAB自动预算
使用GPT-5-Mini自动分类YNAB交易并发送Discord通知
If
Set
Merge
+11
29 节点spencer owen
AI 摘要总结
工作流信息
难度等级
高级
节点数量55
分类2
节点类型16
作者
Artem Boiko
@datadrivenconstructionFounder DataDrivenConstruction.io | AEC Tech Consultant & Automation Expert | Bridging Software and Construction
外部链接
在 n8n.io 查看 →
分享此工作流