8
n8n 中文网amn8n.com

云文档转换为安全控制基线(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
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 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)可能需要您自行付费。

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

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

作者
Raphael De Carvalho Florencio

Raphael De Carvalho Florencio

@followdrabbit

Senior 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 查看

分享此工作流