云文档转换为安全控制基线(OpenAI 和 Google Drive)
高级
这是一个AI Summarization, Multimodal AI领域的自动化工作流,包含 35 个节点。主要使用 If, Set, Code, Switch, Webhook 等节点。 使用 OpenAI 和 Google Drive 将云文档转换为安全控制基线
前置要求
- •HTTP Webhook 端点(n8n 会自动生成)
- •Google Drive API 凭证
- •可能需要目标 API 的认证凭证
- •OpenAI API Key
使用的节点 (35)
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
"meta": {
"instanceId": "247ad01c2ef7bb169c88da0fdc2f39730d8a7e76f71913465e5d08e401bd237a",
"templateCredsSetupCompleted": true
},
"nodes": [
{
"id": "c76e4bd4-dbdf-4e35-808e-b225e66427e6",
"name": "检查必填字段",
"type": "n8n-nodes-base.if",
"position": [
-2624,
128
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "77e24e89-4efa-4d68-b5d3-b8c8252d8834",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json.body.cloudProvider }}",
"rightValue": ""
},
{
"id": "9e527511-acd4-4364-9b28-06a3a03bba50",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json.body.technology }}",
"rightValue": ""
},
{
"id": "d96a8b69-7c38-4519-b706-88ea47442051",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json.body.urls }}",
"rightValue": ""
}
]
},
"looseTypeValidation": true
},
"typeVersion": 2.2
},
{
"id": "a8ff546c-af04-4fef-b7f5-7b55ec65af0f",
"name": "无操作,不执行任何操作",
"type": "n8n-nodes-base.noOp",
"position": [
-2400,
128
],
"parameters": {},
"typeVersion": 1
},
{
"id": "1cbc1ca7-e5db-4e5a-a1f7-e774a70e1201",
"name": "生成唯一标识符",
"type": "n8n-nodes-base.code",
"position": [
-2400,
-64
],
"parameters": {
"jsCode": "function generateShortUUID() {\n return Math.random().toString(36).substring(2, 14); // 12 chars\n}\n\nreturn [\n {\n json: {\n uuid: generateShortUUID()\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "1f8f124a-1417-41b7-abf9-177f9a908cac",
"name": "创建",
"type": "n8n-nodes-base.webhook",
"position": [
-2848,
128
],
"webhookId": "1d225cee-cc5a-43ea-8567-41aeb419acfb",
"parameters": {
"path": "create",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode",
"authentication": "basicAuth"
},
"credentials": {
"httpBasicAuth": {
"id": "YhK2c1HjuoPNeyGw",
"name": "Unnamed credential"
}
},
"typeVersion": 2
},
{
"id": "6ede342c-2ad9-41cc-b11c-59186de696ea",
"name": "更新正文",
"type": "n8n-nodes-base.set",
"position": [
-1504,
-64
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "ff72f1c6-0cea-4bc5-a94c-5f39fff86882",
"name": "uuid",
"type": "string",
"value": "={{ $('generate_uuid').first().json.uuid }}"
},
{
"id": "8844a9ff-7117-4bcb-a726-ad77135ea598",
"name": "cloudprovider",
"type": "string",
"value": "={{ $('create').first().json.body.cloudProvider }}"
},
{
"id": "bf424f91-1487-4c39-aeaf-e6c65471ed33",
"name": "technology",
"type": "string",
"value": "={{ $('create').first().json.body.technology }}"
},
{
"id": "5484b003-dd53-4500-baf6-c13a2d29832e",
"name": "urls",
"type": "array",
"value": "={{ $('create').first().json.body.urls }}"
},
{
"id": "83c4994d-cada-4a58-bf1e-285ab0efeb9e",
"name": "gdrive_target",
"type": "string",
"value": "={{ $('get_gdrive_id').first().json.id }}"
},
{
"id": "d0c2ba9f-274c-4a2f-a95b-ba5d366b0236",
"name": "assistant_extractor_id",
"type": "string",
"value": "={{ $json.assistant_extractor_id }}"
},
{
"id": "0b73530e-5bac-4583-9b3b-8edd642265e0",
"name": "assistant_composer_id",
"type": "string",
"value": "={{ $json.assistant_composer_id }}"
},
{
"id": "9b4cd1e7-8029-4fcd-8079-5d120d00a253",
"name": "assistant_baseline_id",
"type": "string",
"value": "={{ $json.assistant_baseline_id }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "da40bbec-d7ea-4499-a6d7-09762f81c529",
"name": "HTTP获取URL",
"type": "n8n-nodes-base.httpRequest",
"position": [
-832,
-208
],
"parameters": {
"url": "={{ $json.url }}",
"options": {
"timeout": 10000,
"allowUnauthorizedCerts": true
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "User-Agent",
"value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
}
]
}
},
"retryOnFail": true,
"typeVersion": 4.2
},
{
"id": "831e22cb-9d60-41a1-b49e-6ce03f0bee9f",
"name": "HTML清理器",
"type": "n8n-nodes-base.code",
"position": [
-608,
-208
],
"parameters": {
"jsCode": "// 1. Pega o conteúdo HTML\nconst htmlRaw = $json.data || $json.body;\n\nif (!htmlRaw || typeof htmlRaw !== 'string') {\n throw new Error('Campo `data` ou `body` ausente ou inválido.');\n}\n\n// 2. Sanitiza o HTML\nlet cleaned = htmlRaw\n .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, '')\n .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, '')\n .replace(/<!--[\\s\\S]*?-->/g, '')\n .replace(/<(head|header|footer|nav|button|form|aside|meta|link|iframe|noscript)[^>]*>[\\s\\S]*?<\\/\\1>/gi, '')\n .replace(/<[^>]+>/g, '') // remove tags HTML restantes\n .replace(/\\s{2,}/g, ' ')\n .replace(/\\n{2,}/g, '\\n')\n .trim();\n\n// 3. Pega os dados do nó anterior \"process_url\" (sem quebrar o fluxo)\nconst processData = $node[\"process_url\"].json;\n\n// 4. Retorna novo objeto com os metadados + texto sanitizado\nreturn [\n {\n json: {\n uuid: processData.uuid,\n cloudProvider: processData.cloudProvider || processData.cloudprovider,\n technology: processData.technology,\n url: processData.url,\n sanitizedText: cleaned\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "a7f30691-60e2-451f-8855-45053ede5980",
"name": "1_DefySec_提取器",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
-384,
-208
],
"parameters": {
"text": "=CloudProvider: {{ $json.cloudProvider}}\nTechnology: {{ $json.technology }}\nData Source: {{ $json.url }}\nData: {{ $json.sanitizedText }}",
"prompt": "define",
"options": {},
"resource": "assistant",
"assistantId": {
"__rl": true,
"mode": "id",
"value": "={{ $('settings').first().json.assistant_extractor_id }}"
}
},
"credentials": {
"openAiApi": {
"id": "Vkw9T5qrK2yHZxw7",
"name": "OpenAi account"
}
},
"typeVersion": 1.8
},
{
"id": "7278026a-c1ef-49b9-950d-d924e4f2aca9",
"name": "展开URL",
"type": "n8n-nodes-base.code",
"position": [
-1280,
-64
],
"parameters": {
"jsCode": "const { uuid, cloudprovider, technology, urls } = $json;\n\nreturn urls.map(url => ({\n json: {\n uuid,\n cloudProvider: cloudprovider,\n technology,\n url\n }\n}));\n"
},
"typeVersion": 2
},
{
"id": "9e8fecb4-99a5-426f-a09b-245d909be212",
"name": "处理URL",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-1056,
-64
],
"parameters": {
"options": {
"reset": false
}
},
"typeVersion": 3
},
{
"id": "08a7fbde-12c2-4efd-9acf-186a2a61d352",
"name": "搜索文件",
"type": "n8n-nodes-base.googleDrive",
"position": [
192,
-208
],
"parameters": {
"filter": {},
"options": {
"fields": [
"*"
]
},
"resource": "fileFolder",
"returnAll": true,
"queryString": "={{$items(\"settings\")[0].json.uuid}}_extractedControls_"
},
"credentials": {
"googleDriveOAuth2Api": {
"id": "tMSvCpu4CSKuDXkh",
"name": "Google Drive account"
}
},
"typeVersion": 3,
"alwaysOutputData": true
},
{
"id": "012ae0a1-1bf0-4a67-9c1c-1de9a3758f07",
"name": "追加创建过滤器",
"type": "n8n-nodes-base.if",
"position": [
768,
-208
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "180ca863-e6dc-47ca-a211-02ac41530530",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.action }}",
"rightValue": "=append"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "53bb7adc-f2c6-4e5d-b6f4-d0d060c9581a",
"name": "上传新文件",
"type": "n8n-nodes-base.googleDrive",
"position": [
992,
-112
],
"parameters": {
"name": "={{ $json.fileName }}",
"driveId": {
"__rl": true,
"mode": "list",
"value": "My Drive",
"cachedResultUrl": "https://drive.google.com/drive/my-drive",
"cachedResultName": "My Drive"
},
"options": {},
"folderId": {
"__rl": true,
"mode": "id",
"value": "={{ $('settings').first().json.gdrive_target }}"
}
},
"credentials": {
"googleDriveOAuth2Api": {
"id": "tMSvCpu4CSKuDXkh",
"name": "Google Drive account"
}
},
"typeVersion": 3
},
{
"id": "1f9e5eb6-bad4-430e-9998-82a7322dcddf",
"name": "更新现有文件",
"type": "n8n-nodes-base.googleDrive",
"position": [
1440,
-208
],
"parameters": {
"fileId": {
"__rl": true,
"mode": "id",
"value": "={{ $json.fileId }}"
},
"options": {},
"operation": "update",
"changeFileContent": true
},
"credentials": {
"googleDriveOAuth2Api": {
"id": "tMSvCpu4CSKuDXkh",
"name": "Google Drive account"
}
},
"typeVersion": 3
},
{
"id": "df8ef747-d3e1-4d5e-a77d-da693f5baf38",
"name": "下载现有文件",
"type": "n8n-nodes-base.googleDrive",
"position": [
992,
-304
],
"parameters": {
"fileId": {
"__rl": true,
"mode": "id",
"value": "={{ $json.fileId }}"
},
"options": {},
"operation": "download"
},
"credentials": {
"googleDriveOAuth2Api": {
"id": "tMSvCpu4CSKuDXkh",
"name": "Google Drive account"
}
},
"typeVersion": 3
},
{
"id": "42a8de07-049e-4038-b942-d27f7e425bfa",
"name": "合并数据",
"type": "n8n-nodes-base.code",
"position": [
1216,
-304
],
"parameters": {
"jsCode": "// === ec_merge_data — append determinístico (.txt) ===\n\n// 1) Lê o texto atual (prioriza binary.data.data do item corrente; fallback no nó de download)\nfunction readPrevText() {\n // fonte 1: item atual\n let b64 = ($binary?.data && typeof $binary.data.data === 'string') ? $binary.data.data : '';\n\n // fonte 2: nó de download (caso o item atual esteja sem payload)\n if (!b64) {\n try {\n const dl = $items('ec_download_existing_file')[0];\n if (dl?.binary?.data?.data && typeof dl.binary.data.data === 'string') {\n b64 = dl.binary.data.data;\n }\n } catch {}\n }\n\n // fonte 3: raros casos de texto no JSON\n if (!b64) {\n if (typeof $json?.data === 'string') return $json.data.replace(/\\r\\n/g, '\\n');\n if (typeof $json?.body === 'string') return $json.body.replace(/\\r\\n/g, '\\n');\n if (typeof $json?.content === 'string') return $json.content.replace(/\\r\\n/g, '\\n');\n return '';\n }\n\n try { return Buffer.from(b64, 'base64').toString('utf8').replace(/\\r\\n/g, '\\n'); }\n catch { return ''; }\n}\n\nconst prevText = readPrevText();\n\n// 2) Texto novo direto do \"1_DefySec_Extractor\"\nlet ext;\ntry { ext = $items('1_DefySec_Extractor')[0]?.json; } catch {}\nif (!ext) { try { ext = $node['1_DefySec_Extractor']?.json; } catch {} }\n\nlet newText = ext?.output ?? ext?.data ?? ext?.text ?? '';\nnewText = String(newText)\n .replace(/^```(?:txt|text|json)?\\s*/i, '') // remove cercas, se vierem\n .replace(/\\s*```$/, '')\n .replace(/\\r\\n/g, '\\n');\n\n// 3) Append (1 linha em branco entre blocos quando já existe conteúdo)\nconst combined = prevText\n ? prevText.replace(/\\s*$/, '') + '\\n\\n' + newText.replace(/^\\s+/, '')\n : newText;\n\n// 4) Empacota binário para o Update (sempre em binary.data)\nconst outB64 = Buffer.from(combined, 'utf8').toString('base64');\n\n// 5) Garante fileId/fileName para o Update\nlet { fileId, fileName } = $json;\nif (!fileId || !fileName) {\n try {\n const src = $items('extracted_controls_append_or_create')[0]?.json;\n fileId = fileId || src?.fileId;\n fileName = fileName || src?.fileName;\n } catch {}\n}\n\n// 6) **RETORNA UM ARRAY** com 1 item (é isso que o n8n exige)\nreturn [{\n json: {\n fileId,\n fileName,\n prevBytes: prevText.length,\n newBytes: newText.length,\n mergedBytes: combined.length\n },\n binary: {\n data: {\n data: outB64,\n mimeType: 'text/plain',\n fileName\n }\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "ce20266e-90b4-4d75-b3d4-6777729e7e5f",
"name": "提取文件信息",
"type": "n8n-nodes-base.code",
"position": [
480,
-208
],
"parameters": {
"jsCode": "// === Code: decide append/create e prepara dados ===\n\n// util\nfunction safe(node) {\n try { const a = $items(node); if (a?.[0]?.json) return a[0].json; } catch {}\n try { const j = $node[node]?.json; if (j) return j; } catch {}\n return {};\n}\n\n// 1) Contexto\nconst settings = safe('settings');\nconst uuid = String(settings.uuid || '').trim();\nconst folderId = String(settings.gdrive_target || '').trim();\nif (!uuid) throw new Error('uuid ausente no nó \"settings\".');\nif (!folderId) throw new Error('gdrive_target (folderId) ausente no nó \"settings\".');\n\nconst canonicalName = `${uuid}_extractedControls.txt`;\n\n// 2) Normaliza retorno do List para array de arquivos\nconst files = [];\nfor (const it of items) {\n const j = it?.json;\n if (Array.isArray(j)) files.push(...j);\n else if (j && (j.name || j.id)) files.push(j);\n}\nconst existing = files.find(f => f.name === canonicalName);\n\n// 3) Pega TEXTO do \"1_DefySec_Extractor\"\nconst ext = safe('1_DefySec_Extractor');\nlet newText = ext.output ?? ext.data ?? ext.text ?? $json.output ?? '';\nnewText = String(newText)\n .replace(/^```(?:txt|text|json)?\\s*/i, '')\n .replace(/\\s*```$/, '')\n .replace(/\\r\\n/g, '\\n');\n\n// 4) Decide ação\nconst out = {\n action: existing ? 'append' : 'create',\n fileId: existing?.id || null,\n fileName: canonicalName,\n folderId,\n newText,\n};\n\n// 5) Se for criação, já emite binário pra Upload; se for append, só metadados.\nif (!existing) {\n const base64 = Buffer.from(newText, 'utf8').toString('base64');\n return [{\n json: out,\n binary: {\n data: {\n data: base64,\n mimeType: 'text/plain',\n fileName: canonicalName\n }\n }\n }];\n}\n\nreturn [{ json: out }];"
},
"typeVersion": 2
},
{
"id": "cad3694b-fbc6-4d97-a0b5-02f8f2601f89",
"name": "搜索文件",
"type": "n8n-nodes-base.googleDrive",
"position": [
-832,
-784
],
"parameters": {
"filter": {},
"options": {
"fields": [
"*"
]
},
"resource": "fileFolder",
"returnAll": true,
"queryString": "={{$items(\"settings\")[0].json.uuid}}_extractedControls.txt"
},
"credentials": {
"googleDriveOAuth2Api": {
"id": "tMSvCpu4CSKuDXkh",
"name": "Google Drive account"
}
},
"typeVersion": 3,
"alwaysOutputData": true
},
{
"id": "1c259737-687a-4862-8052-24290b3f4557",
"name": "提取文件信息",
"type": "n8n-nodes-base.code",
"position": [
-608,
-784
],
"parameters": {
"jsCode": "// === ec_extract_content — lê do Google Drive (ou do item atual) e repassa ===\n// Saída: [{ json: { content, fileId, fileName, mimeType, length } }]\n\nfunction readFromCurrentItem() {\n const b = $binary?.data;\n if (b?.data && typeof b.data === 'string') {\n return {\n b64: b.data,\n fileName: b.fileName,\n mimeType: b.mimeType,\n };\n }\n return null;\n}\n\nfunction readFromDownloadNode() {\n try {\n const dl = $items('ec_download_existing_file')[0];\n const b = dl?.binary?.data;\n if (b?.data && typeof b.data === 'string') {\n // fileId/fileName podem também estar em dl.json dependendo do seu fluxo\n return {\n b64: b.data,\n fileName: b.fileName || dl?.json?.fileName,\n mimeType: b.mimeType || dl?.json?.mimeType,\n fileId: dl?.json?.fileId,\n };\n }\n } catch {}\n return null;\n}\n\nfunction readTextFallback() {\n // Casos raros em que veio texto no JSON\n if (typeof $json?.data === 'string') return $json.data;\n if (typeof $json?.body === 'string') return $json.body;\n if (typeof $json?.content === 'string') return $json.content;\n return '';\n}\n\nfunction b64ToUtf8(b64) {\n try {\n return Buffer.from(b64, 'base64').toString('utf8');\n } catch {\n return '';\n }\n}\n\nfunction normalize(text) {\n return String(text)\n .replace(/^\\uFEFF/, '') // remove BOM, se houver\n .replace(/^```(?:txt|text|json)?\\s*/i, '') // remove cercas de código no início\n .replace(/\\s*```$/, '') // remove cerca de fechamento\n .replace(/\\r\\n/g, '\\n'); // normaliza quebras de linha\n}\n\n// --- Coleta do conteúdo ---\nlet meta = readFromCurrentItem();\nif (!meta) meta = readFromDownloadNode();\n\nlet text = '';\nlet fileName, mimeType, fileId;\n\nif (meta?.b64) {\n text = b64ToUtf8(meta.b64);\n fileName = meta.fileName;\n mimeType = meta.mimeType;\n fileId = meta.fileId;\n} else {\n text = readTextFallback();\n}\n\n// Normaliza e prepara saída\ntext = normalize(text);\n\n// Tenta herdar fileId/fileName do JSON atual se não vieram do download\nif (!fileId) fileId = $json?.fileId;\nif (!fileName) fileName = $json?.fileName;\nif (!mimeType) mimeType = $json?.mimeType || 'text/plain';\n\nreturn [{\n json: {\n content: text,\n fileId,\n fileName,\n mimeType,\n length: text.length,\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "2f8fb3d1-5bbd-4d3b-affc-b2090abf83a0",
"name": "2_DefySec_控制组合器",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
-384,
-784
],
"parameters": {
"text": "=CloudProvider: {{ $('settings').first().json.cloudprovider }}\nTechnology: {{ $('settings').first().json.technology }}\n\n{{ $json.content }}",
"prompt": "define",
"options": {},
"resource": "assistant",
"assistantId": {
"__rl": true,
"mode": "id",
"value": "={{ $('settings').first().json.assistant_composer_id }}"
}
},
"credentials": {
"openAiApi": {
"id": "Vkw9T5qrK2yHZxw7",
"name": "OpenAi account"
}
},
"typeVersion": 1.8
},
{
"id": "8c37cafd-51d0-419b-ba2c-ae0b3b54a8c9",
"name": "检查控制项",
"type": "n8n-nodes-base.if",
"position": [
-32,
-208
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "f1eb6945-e389-44de-b327-d93afef4a987",
"operator": {
"type": "string",
"operation": "notEquals"
},
"leftValue": "={{ $json.output }}",
"rightValue": "NO_CONTROLS_FOUND"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "cb935c32-d1b2-4566-baf1-316f95ac26aa",
"name": "控制路由",
"type": "n8n-nodes-base.switch",
"position": [
-32,
-784
],
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "69d835f1-aa34-4931-8b40-91088a9cf68a",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.output }}",
"rightValue": "NO_CONTROLS_FOUND"
}
]
}
}
]
},
"options": {
"fallbackOutput": "extra"
}
},
"typeVersion": 3.2
},
{
"id": "f1d5fd0e-e481-4b91-af07-804d02098c07",
"name": "无控制项应答",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
192,
-784
],
"parameters": {
"options": {},
"respondWith": "json",
"responseBody": "{\n \"result\": \"NO_CONTROLS_FOUND\",\n \"message\": \"Nenhum controle válido foi identificado. O arquivo está vazio ou não contém blocos no padrão esperado (Description, Reference, SecurityObjective) ou o cabeçalho CloudProvider/Technology está ausente.\",\n \"next_steps\": [\n \"Garanta as duas primeiras linhas: 'CloudProvider:' e 'Technology:'.\",\n \"Inclua ao menos um bloco válido com Description, Reference (URL) e SecurityObjective.\",\n \"Remova textos/JSONs fora do padrão entre os blocos.\"\n ]\n}"
},
"typeVersion": 1.4
},
{
"id": "c7e68f8c-abe4-443f-a65f-efef1a5e3b6f",
"name": "3_DefySec 基线构建器",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
416,
-592
],
"parameters": {
"text": "=CloudProvider: {{ $('settings').first().json.cloudprovider }}\nTechnology: {{ $('settings').first().json.technology }}\n\n{{ $json.output }}",
"prompt": "define",
"options": {},
"resource": "assistant",
"assistantId": {
"__rl": true,
"mode": "id",
"value": "={{ $('settings').first().json.assistant_baseline_id }}"
}
},
"credentials": {
"openAiApi": {
"id": "Vkw9T5qrK2yHZxw7",
"name": "OpenAi account"
}
},
"typeVersion": 1.8
},
{
"id": "56553247-2b0d-44ae-a8df-d3c2f42f10ef",
"name": "检查控制项",
"type": "n8n-nodes-base.code",
"position": [
192,
-592
],
"parameters": {
"jsCode": "// === cc_route_on_no_controls ===\n// Se output === \"NO_CONTROLS_TO_BE_CONSOLIDATED\", substitui o payload pelo json de cc_extract_file_info.\n// Senão, apenas repassa o item original.\n//\n// Compatível com:\n// 1) { json: { output: \"NO_CONTROLS_TO_BE_CONSOLIDATED\", threadId: \"...\" } }\n// 2) { json: [ { output: \"NO_CONTROLS_TO_BE_CONSOLIDATED\", threadId: \"...\" } ] }\n\nfunction getOutputValue(payload) {\n if (payload == null) return '';\n if (Array.isArray(payload)) {\n const first = payload[0];\n return typeof first?.output === 'string' ? first.output.trim() : '';\n }\n if (typeof payload === 'object') {\n return typeof payload.output === 'string' ? payload.output.trim() : '';\n }\n if (typeof payload === 'string') return payload.trim();\n return '';\n}\n\nfunction getThreadId(payload) {\n if (payload == null) return undefined;\n if (Array.isArray(payload)) return payload[0]?.threadId;\n if (typeof payload === 'object') return payload.threadId;\n return undefined;\n}\n\nfunction getCcInfo() {\n try {\n const n = $items('cc_extract_file_info')[0];\n const j = n?.json ?? {};\n\n // settings e uuid (uuid pode estar dentro de settings ou na raiz, por segurança)\n const settings = j.settings ?? {};\n const uuid = String((settings.uuid ?? j.uuid ?? '')).trim();\n\n // opcionalmente preserva alguns metadados úteis se existirem\n const meta = {};\n for (const k of ['fileId', 'fileName', 'mimeType', 'path', 'size']) {\n if (j[k] !== undefined) meta[k] = j[k];\n }\n\n return { settings, uuid, ...meta };\n } catch {\n return null;\n }\n}\n\nlet itemsIn;\ntry {\n itemsIn = $input.all(); // n8n Code node novo\n} catch {\n // fallback (algumas versões)\n itemsIn = [{ json: $json, binary: $binary }];\n}\n\nconst itemsOut = itemsIn.map((item) => {\n const outVal = getOutputValue(item.json);\n const outValUC = outVal.toUpperCase();\n\n if (outValUC === 'NO_CONTROLS_TO_BE_CONSOLIDATED') {\n const info = getCcInfo();\n // Se não conseguir ler cc_extract_file_info, mantém o item original para não quebrar o fluxo\n if (!info) return item;\n\n // opcional: mantém o threadId original (se existir) para rastreabilidade\n const threadId = getThreadId(item.json);\n if (threadId) info.threadId = threadId;\n\n return { json: info };\n }\n\n // Qualquer outra resposta: apenas passa adiante sem alterações\n return item;\n});\n\nreturn itemsOut;\n"
},
"typeVersion": 2
},
{
"id": "7509b484-dbb7-416e-b7a5-c1fc02faf16a",
"name": "检查控制项",
"type": "n8n-nodes-base.if",
"position": [
768,
-592
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "f1eb6945-e389-44de-b327-d93afef4a987",
"operator": {
"type": "string",
"operation": "notEquals"
},
"leftValue": "={{ $json.output }}",
"rightValue": "NO_CONTROLS_FOUND"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "1f9c0594-b3e0-41e2-9a9e-003bcc60c828",
"name": "无控制项应答",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
992,
-496
],
"parameters": {
"options": {},
"respondWith": "json",
"responseBody": "{\n \"result\": \"NO_CONTROLS_FOUND\",\n \"message\": \"Nenhum controle válido foi identificado. O arquivo está vazio ou não contém blocos no padrão esperado (Description, Reference, SecurityObjective) ou o cabeçalho CloudProvider/Technology está ausente.\",\n \"next_steps\": [\n \"Garanta as duas primeiras linhas: 'CloudProvider:' e 'Technology:'.\",\n \"Inclua ao menos um bloco válido com Description, Reference (URL) e SecurityObjective.\",\n \"Remova textos/JSONs fora do padrão entre os blocos.\"\n ]\n}"
},
"typeVersion": 1.4
},
{
"id": "5ea33aaf-7f97-4ceb-97e0-1023405aad4b",
"name": "数据准备",
"type": "n8n-nodes-base.code",
"position": [
992,
-688
],
"parameters": {
"jsCode": "// === make_file_from_output ===\n// Lê o(s) \"output(s)\" recebido(s) e gera 1 arquivo texto em binary.data\n// Saída: [{ json: { fileName, mimeType, bytes }, binary: { data: { data, mimeType, fileName } } }]\n\nfunction extractOutputFromJson(j) {\n if (j == null) return '';\n if (typeof j === 'string') return j;\n\n if (Array.isArray(j)) {\n // junta todos os outputs de um array (se houver mais de um)\n const parts = j\n .map(o => (o && typeof o.output === 'string') ? o.output : '')\n .filter(Boolean);\n if (parts.length) return parts.join('\\n\\n---\\n\\n');\n // fallback: se o array não tem \"output\", tenta serializar\n try { return JSON.stringify(j, null, 2); } catch { return ''; }\n }\n\n if (typeof j === 'object') {\n if (typeof j.output === 'string') return j.output;\n if (typeof j.content === 'string') return j.content;\n if (typeof j.data === 'string') return j.data;\n if (typeof j.body === 'string') return j.body;\n try { return JSON.stringify(j, null, 2); } catch {}\n }\n return '';\n}\n\nfunction stripCodeFences(s) {\n return String(s)\n .replace(/^\\s*```[a-z]*\\s*/i, '') // remove ```txt / ```json / ``` etc no início\n .replace(/\\s*```[\\s\\r\\n]*$/i, '') // remove ``` no fim\n .replace(/^\\uFEFF/, '') // remove BOM se houver\n .replace(/\\r\\n/g, '\\n'); // normaliza quebras\n}\n\nfunction detectTechnology(s) {\n const m = s.match(/Technology:\\s*([^\\n]+)/i);\n return m ? m[1].trim().replace(/[^\\w.-]+/g, '_') : null;\n}\n\nconst itemsIn = $input.all();\n\n// Coleta e normaliza conteúdos\nconst contents = [];\nfor (const it of itemsIn) {\n const raw = extractOutputFromJson(it.json);\n if (!raw) continue;\n contents.push(stripCodeFences(raw));\n}\n\nconst content = (contents.join('\\n\\n---\\n\\n') || '').trim();\n\n// Gera nome de arquivo\nconst tech = detectTechnology(content) || 'output';\nconst ts = new Date().toISOString().replace(/[:.]/g, '-'); // seguro para filename\nconst fileName = `controls_${tech}_${ts}.txt`;\nconst mimeType = 'text/plain';\n\n// Se não houver conteúdo, ainda retornamos um arquivo com nota (evita quebrar o fluxo)\nconst finalText = content || 'NO_CONTENT_EXTRACTED';\nconst bytes = Buffer.byteLength(finalText, 'utf8');\nconst b64 = Buffer.from(finalText, 'utf8').toString('base64');\n\n// Retorno no formato esperado pelo n8n\nreturn [{\n json: { fileName, mimeType, bytes },\n binary: {\n data: {\n data: b64,\n mimeType,\n fileName,\n },\n },\n}];\n"
},
"typeVersion": 2
},
{
"id": "c1e047f4-83f6-45e9-a550-3e54c10eb919",
"name": "获取 Google Drive ID",
"type": "n8n-nodes-base.googleDrive",
"position": [
-2176,
-64
],
"parameters": {
"filter": {},
"options": {},
"resource": "fileFolder",
"queryString": "n8n_defysec"
},
"credentials": {
"googleDriveOAuth2Api": {
"id": "tMSvCpu4CSKuDXkh",
"name": "Google Drive account"
}
},
"typeVersion": 3
},
{
"id": "6a21ef73-0859-4b51-8dc9-11b70af0a8d9",
"name": "解析助手",
"type": "n8n-nodes-base.code",
"position": [
-1728,
-64
],
"parameters": {
"jsCode": "// Lê todos os itens de entrada do nó anterior\nconst all = $input.all(); // [{json: {...}}, ...]\n\n// Normaliza: pode ser (a) vários itens simples, (b) 1 item com `data[]`, (c) 1 item com array plano\nlet list;\nif (all.length === 1 && Array.isArray(all[0].json?.data)) {\n list = all[0].json.data; // caso: { data: [...] }\n} else if (all.length === 1 && Array.isArray(all[0].json)) {\n list = all[0].json; // caso: [{id,name,model}, ...] dentro do json\n} else {\n list = all.map(i => i.json); // caso: cada item já é {id,name,model}\n}\n\n// Helpers: escolha por nome (ajuste os regex se renomear seus assistants)\nconst pick = (re) => list.find(a => re.test(String(a.name || '')));\n\n// Heurística pelos seus nomes:\nconst extractor = pick(/(^|[\\s_-])1[\\s_-]*DefySec[\\s_-]*Extractor\\b/i) || pick(/\\bExtractor\\b/i);\nconst composer = pick(/(^|[\\s_-])2[\\s_-]*DefySec[\\s_-]*Control[\\s_-]*Composer\\b/i) || pick(/\\bComposer\\b/i);\nconst baseline = pick(/(^|[\\s_-])3[\\s_-]*DefySec[\\s_-]*Baseline\\b/i) || pick(/\\bBaseline\\b/i);\n\n// Saída única com os IDs resolvidos\nreturn [\n {\n json: {\n assistant_extractor_id: extractor?.id || '',\n assistant_composer_id: composer?.id || '',\n assistant_baseline_id: baseline?.id || ''\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "85305d1a-d4de-4917-9f16-09000677a767",
"name": "OpenAI_助手列表",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
-1952,
-64
],
"parameters": {
"resource": "assistant",
"operation": "list"
},
"credentials": {
"openAiApi": {
"id": "Vkw9T5qrK2yHZxw7",
"name": "OpenAi account"
}
},
"typeVersion": 1.8
},
{
"id": "08f933aa-5892-442a-83c6-5e5d365ca0e0",
"name": "数据响应",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
1216,
-688
],
"parameters": {
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Disposition",
"value": "=attachment; filename=\"{{ $binary.data.fileName }}\""
}
]
}
},
"respondWith": "binary"
},
"typeVersion": 1.4
},
{
"id": "bfd45e14-84e0-44d5-84f8-c90527f2efed",
"name": "便签",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2928,
-672
],
"parameters": {
"color": 5,
"width": 608,
"height": 336,
"content": "## 概述"
},
"typeVersion": 1
},
{
"id": "c7496163-e016-4565-ab9e-adf1ce42e0a5",
"name": "便签1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2288,
-672
],
"parameters": {
"color": 5,
"width": 608,
"height": 336,
"content": "## 设置与凭据"
},
"typeVersion": 1
},
{
"id": "5a7828b9-f6d5-4636-be1a-a1c69272d0d1",
"name": "便签2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1648,
-672
],
"parameters": {
"color": 5,
"width": 608,
"height": 336,
"content": "## 运行与故障排除"
},
"typeVersion": 1
}
],
"pinData": {},
"connections": {
"create": {
"main": [
[
{
"node": "check_mandatory_fields",
"type": "main",
"index": 0
}
]
]
},
"settings": {
"main": [
[
{
"node": "explode_urls",
"type": "main",
"index": 0
}
]
]
},
"process_url": {
"main": [
[
{
"node": "cc_search_files",
"type": "main",
"index": 0
}
],
[
{
"node": "http_get_url",
"type": "main",
"index": 0
}
]
]
},
"bb_data_prep": {
"main": [
[
{
"node": "bb_data_respond",
"type": "main",
"index": 0
}
]
]
},
"explode_urls": {
"main": [
[
{
"node": "process_url",
"type": "main",
"index": 0
}
]
]
},
"http_get_url": {
"main": [
[
{
"node": "html_sanitizer",
"type": "main",
"index": 0
}
]
]
},
"ec_merge_data": {
"main": [
[
{
"node": "ec_update_existing_file",
"type": "main",
"index": 0
}
]
]
},
"generate_uuid": {
"main": [
[
{
"node": "get_gdrive_id",
"type": "main",
"index": 0
}
]
]
},
"get_gdrive_id": {
"main": [
[
{
"node": "OpenAI_Assistants_List",
"type": "main",
"index": 0
}
]
]
},
"html_sanitizer": {
"main": [
[
{
"node": "1_DefySec_Extractor",
"type": "main",
"index": 0
}
]
]
},
"cc_search_files": {
"main": [
[
{
"node": "cc_extract_file_info",
"type": "main",
"index": 0
}
]
]
},
"ec_search_files": {
"main": [
[
{
"node": "ec_extract_file_info",
"type": "main",
"index": 0
}
]
]
},
"bb_controls_check": {
"main": [
[
{
"node": "bb_data_prep",
"type": "main",
"index": 0
}
],
[
{
"node": "bb_no_controls_answer",
"type": "main",
"index": 0
}
]
]
},
"cc_controls_check": {
"main": [
[
{
"node": "3_DefySec Baseline Builder",
"type": "main",
"index": 0
}
]
]
},
"ec_controls_check": {
"main": [
[
{
"node": "ec_search_files",
"type": "main",
"index": 0
}
],
[
{
"node": "process_url",
"type": "main",
"index": 0
}
]
]
},
"cc_controls_router": {
"main": [
[
{
"node": "cc_no_controls_answer",
"type": "main",
"index": 0
}
],
[
{
"node": "cc_controls_check",
"type": "main",
"index": 0
}
]
]
},
"ec_upload_new_file": {
"main": [
[
{
"node": "process_url",
"type": "main",
"index": 0
}
]
]
},
"resolve_assistants": {
"main": [
[
{
"node": "settings",
"type": "main",
"index": 0
}
]
]
},
"1_DefySec_Extractor": {
"main": [
[
{
"node": "ec_controls_check",
"type": "main",
"index": 0
}
]
]
},
"cc_extract_file_info": {
"main": [
[
{
"node": "2_DefySec_Control_Composer",
"type": "main",
"index": 0
}
]
]
},
"ec_extract_file_info": {
"main": [
[
{
"node": "ec_append_create_filter",
"type": "main",
"index": 0
}
]
]
},
"OpenAI_Assistants_List": {
"main": [
[
{
"node": "resolve_assistants",
"type": "main",
"index": 0
}
]
]
},
"check_mandatory_fields": {
"main": [
[
{
"node": "generate_uuid",
"type": "main",
"index": 0
}
],
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
},
"ec_append_create_filter": {
"main": [
[
{
"node": "ec_download_existing_file",
"type": "main",
"index": 0
}
],
[
{
"node": "ec_upload_new_file",
"type": "main",
"index": 0
}
]
]
},
"ec_update_existing_file": {
"main": [
[
{
"node": "process_url",
"type": "main",
"index": 0
}
]
]
},
"ec_download_existing_file": {
"main": [
[
{
"node": "ec_merge_data",
"type": "main",
"index": 0
}
]
]
},
"2_DefySec_Control_Composer": {
"main": [
[
{
"node": "cc_controls_router",
"type": "main",
"index": 0
}
]
]
},
"3_DefySec Baseline Builder": {
"main": [
[
{
"node": "bb_controls_check",
"type": "main",
"index": 0
}
]
]
}
}
}常见问题
如何使用这个工作流?
复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。
这个工作流适合什么场景?
高级 - AI 摘要总结, 多模态 AI
需要付费吗?
本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。
相关工作流推荐
每日 WhatsApp 群组智能分析:GPT-4.1 分析与语音消息转录
每日 WhatsApp 群组智能分析:GPT-4.1 分析与语音消息转录
If
Set
Code
+20
52 节点Daniel Lianes
杂项
Telegram AI歌词学习机器人 — 翻译、摘要、词汇
Telegram AI歌词学习机器人 — 翻译、摘要、词汇
If
Set
Code
+7
30 节点Raphael De Carvalho Florencio
内容创作
[模板] AI宠物店 v8
🐶 AI宠物店助手 - 集成GPT-4o、Google日历和WhatsApp/Instagram/Facebook
If
N8n
Set
+38
244 节点Amanda Benks
销售
AI 代理餐厅 [模板]
🤖 WhatsApp、Instagram 和 Messenger 的 AI 餐厅助手
If
N8n
Set
+37
239 节点Amanda Benks
其他
使用Airtable、OpenAI和Unipile的自动化LinkedIn潜在客户生成与私信触达
使用Airtable、OpenAI和Unipile的自动化LinkedIn潜在客户生成与私信触达
If
Set
Code
+15
143 节点Ruben AI
客户培育
批量SEO内容生成与带AI图片的Webflow草稿创建(模板)
使用GPT、Gemini图片和Webflow草稿创建进行批量SEO内容生成
If
Set
Code
+18
54 节点Dahiana
内容创作
工作流信息
难度等级
高级
节点数量35
分类2
节点类型12
作者
Raphael De Carvalho Florencio
@followdrabbitSenior Information Security Architect transitioning into AI/RPA and productized workflows. I love to innovate and test ideas—prototyping with n8n, lovable, ChatGPT, and CrewAI to turn repetitive tasks into reusable, documented templates.
外部链接
在 n8n.io 查看 →
分享此工作流