BrightData每周对比
高级
这是一个AI, Marketing领域的自动化工作流,包含 35 个节点。主要使用 If, Set, Code, Gmail, Markdown 等节点,结合人工智能技术实现智能自动化。 使用Bright Data、GPT-4.1和Google Workspace的自动化网站变更监控
前置要求
- •Google 账号和 Gmail API 凭证
- •Google Drive API 凭证
- •Google Sheets API 凭证
- •OpenAI API Key
使用的节点 (35)
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
"id": "pNaD8QIgVGDqbCoU",
"meta": {
"instanceId": "3af183a3db355380be4f6d2f3dfb18bdaa750e90f99a48f91bd71080ee6bcbe8",
"templateCredsSetupCompleted": true
},
"name": "BrightData 每周对比",
"tags": [],
"nodes": [
{
"id": "a2966ed3-e998-47ca-83b3-b7dc4832bc8f",
"name": "多表:您可以连接多个表以实现有组织的数据结构",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
700,
140
],
"parameters": {
"jsonSchemaExample": "{\n \"filename\": \"domain-path-DD-MM-YYYY.md\",\n \"metadata\": {\n \"pageTitle\": \"The primary title of the webpage\",\n \"metaDescription\": \"The meta description content\"\n },\n \"headings\": {\n \"h1\": [\"List of all H1 headings\"],\n \"h2\": [\"List of all H2 headings\"],\n \"h3\": [\"List of all H3 headings\"]\n },\n \"pricing\": [\n {\n \"planName\": \"Name of the pricing plan/tier\",\n \"currency\": \"USD/EUR/GBP/etc.\",\n \"interval\": \"monthly/yearly/one-time\",\n \"price\": \"Numerical price value\",\n \"features\": [\"List of features or benefits included in this plan\"]\n }\n ],\n \"navigation\": {\n \"mainMenu\": [\"Primary navigation items\"],\n \"subMenu\": [\"Secondary navigation items if present\"]\n },\n \"callToAction\": [\"All CTA elements and their text\"],\n \"contactInfo\": {\n \"phone\": [\"Phone numbers found\"],\n \"email\": [\"Email addresses found\"],\n \"address\": [\"Physical addresses found\"],\n \"formPresent\": true\n },\n \"banners\": [\"Content from promotional banners\"],\n \"faq\": [\n {\n \"question\": \"FAQ question\",\n \"answer\": \"FAQ answer\"\n }\n ]\n}"
},
"typeVersion": 1.2
},
{
"id": "6d4b38bd-fb58-4675-9b83-ade043914d65",
"name": "设置工作流变量",
"type": "n8n-nodes-base.set",
"position": [
-260,
-120
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "96347cce-576a-413c-9c9b-b2257eced54d",
"name": "DriveFolderID",
"type": "string",
"value": ""
},
{
"id": "b2f5e75f-575d-40ed-89a8-1c2e537d3220",
"name": "ComparisonSpreadsheetID",
"type": "string",
"value": ""
},
{
"id": "f64718d6-a5f2-491e-abec-b26a6e6125e8",
"name": "ComparisonSpreadsheetSheetName",
"type": "string",
"value": "Sheet1"
},
{
"id": "1a71595c-58c3-4eda-9b7b-5c306123db86",
"name": "Email",
"type": "string",
"value": ""
},
{
"id": "ed5fe6bd-b43b-45b1-b1e8-b20beb329e4d",
"name": "IsTest",
"type": "boolean",
"value": false
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "6ddda34f-556e-49a5-a103-7641a3f0598d",
"name": "将工作流变量与 Google Sheets 数据合并",
"type": "n8n-nodes-base.set",
"position": [
180,
-120
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "e129ca16-8675-485e-9f21-e5d22b76c6e6",
"name": "ComparisonSpreadsheetFileName",
"type": "string",
"value": "={{ $('Set workflow variables').item.json.ComparisonSpreadsheetFileName }}"
},
{
"id": "5fdee148-82eb-4542-9fe7-1ba37ec09571",
"name": "ComparisonSpreadsheetSheetName",
"type": "string",
"value": "={{ $('Set workflow variables').item.json.ComparisonSpreadsheetSheetName }}"
},
{
"id": "7cce5924-a67a-45a2-8792-cd9ad28834b8",
"name": "Email",
"type": "string",
"value": "={{ $('Set workflow variables').item.json.Email }}"
},
{
"id": "e445b570-384f-4558-99d3-12802b0fa900",
"name": "DriveFolderID",
"type": "string",
"value": "={{ $('Set workflow variables').item.json.DriveFolderID }}"
},
{
"id": "d487666e-4dd3-4d20-8769-d0daf3bab268",
"name": "IsTest",
"type": "boolean",
"value": "={{ $('Set workflow variables').item.json.IsTest }}"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "45eefb28-dce0-4fc3-81a1-865762c73226",
"name": "从对比电子表格读取数据",
"type": "n8n-nodes-base.googleSheets",
"position": [
-40,
-120
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "name",
"value": "={{ $json.ComparisonSpreadsheetSheetName }}"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "={{ $json.ComparisonSpreadsheetID }}"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": "vnXMSwscCP06bp9u",
"name": "Google Sheets account"
}
},
"typeVersion": 4.5
},
{
"id": "c04cec36-1664-48d7-a9ae-a5665bc2c188",
"name": "循环处理每个对比 URL",
"type": "n8n-nodes-base.splitInBatches",
"position": [
400,
-115
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "c50e3ec4-c3bf-4417-94ad-5b57ad6424da",
"name": "网页抓取和数据提取代理",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
652,
-365
],
"parameters": {
"text": "=Please scrape the following url {{ $json.URL }}\n",
"options": {
"systemMessage": "=## Role\nYou are an expert Web Data Extraction Specialist with a specialization in content analysis, information architecture, and structured data organization.\n\n## Task\nYour primary task is to extract, organize, and present key website components from any URL provided by the user.\nTo achieve this, you will need to perform the following:\n- Scrape the target webpage using the scrape_as_markdown tool to obtain its content\n- Generate a clean filename based on the URL and current date for storing the content as JSON\n- Systematically extract and categorize the following elements from the markdown content\n- Format the extracted data into a structured JSON object containing the processed elements\n\n## Tools\nYou can use the following tools:\n1. **scrape_as_markdown**: Use this tool to extract the full content of a webpage in Markdown format. This tool can bypass bot detection and CAPTCHA systems to ensure reliable extraction.\n\n## Input\nYou will receive input as a URL to the webpage that requires data extraction.\n\n## Output\nYour output must be a single, valid JSON object containing all extracted elements and a clean filename. Do not include any explanatory text before or after the JSON.\n\n{\n \"filename\": \"domain-path-DD-MM-YYYY.json\",\n \"metadata\": {\n \"pageTitle\": \"The primary title of the webpage\",\n \"metaDescription\": \"The meta description content\"\n },\n \"headings\": {\n \"h1\": [\"List of all H1 headings\"],\n \"h2\": [\"List of all H2 headings\"],\n \"h3\": [\"List of all H3 headings\"]\n },\n \"pricing\": [\n {\n \"planName\": \"Name of the pricing plan/tier\",\n \"currency\": \"USD/EUR/GBP/etc.\",\n \"interval\": \"monthly/yearly/one-time\",\n \"price\": \"Numerical price value\",\n \"features\": [\"List of features or benefits included in this plan\"]\n }\n ],\n \"navigation\": {\n \"mainMenu\": [\"Primary navigation items\"],\n \"subMenu\": [\"Secondary navigation items if present\"]\n },\n \"callToAction\": [\"All CTA elements and their text\"],\n \"contactInfo\": {\n \"phone\": [\"Phone numbers found\"],\n \"email\": [\"Email addresses found\"],\n \"address\": [\"Physical addresses found\"],\n \"formPresent\": true/false\n },\n \"banners\": [\"Content from promotional banners\"],\n \"faq\": [\n {\n \"question\": \"FAQ question\",\n \"answer\": \"FAQ answer\"\n }\n ]\n}\n\nFor any elements not found on the webpage, include an empty array [] or appropriate null value. Ensure the JSON is properly formatted and valid. If the extraction process encounters any errors, include an additional \"errors\" key with relevant details.\n\nWhen extracting pricing information, analyze the webpage carefully to identify all pricing plans. For each plan, determine:\n- The name of the plan (e.g., \"Basic\", \"Pro\", \"Enterprise\")\n- The currency symbol or code used\n- Whether the pricing is monthly, yearly, or one-time\n- The numerical price value\n- All features or details associated with that pricing tier\n\nThe filename should be generated using the following format:\n- Extract the domain name from the URL (without www.)\n- Extract the path (without any query parameters or fragments)\n- Replace slashes with hyphens\n- Add the current date in DD-MM-YYYY format\n- Use .json as the file extension\n- For example, if the URL is https://asana.com/pricing and today is May 20, 2025, the filename would be \"asana-pricing-20-05-2025.json\"\n- Today's date is {{ $now }}\n\nEven if some pricing information is ambiguous or incomplete, make reasonable inferences and include all relevant details in the structured format."
},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 1.9
},
{
"id": "c1f69dc5-6612-4575-aaf4-1d6a7a70c423",
"name": "上传本周 JSON 文件",
"type": "n8n-nodes-base.googleDrive",
"position": [
1280,
-365
],
"parameters": {
"name": "={{ $('Web scraping and data extraction agent').item.json.output.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": "={{ $('Loop over each comparison URL').item.json.DriveFolderID }}"
}
},
"credentials": {
"googleDriveOAuth2Api": {
"id": "ENsCK6J7JBSny3Pv",
"name": "Google Drive account"
}
},
"typeVersion": 3
},
{
"id": "e4a1fd2c-e1a5-4065-a9c8-76d1c854af95",
"name": "使用本周文件数据更新对比表格",
"type": "n8n-nodes-base.googleSheets",
"position": [
1500,
-365
],
"parameters": {
"columns": {
"value": {
"URL": "={{ $('Loop over each comparison URL').item.json.URL }}",
"Previous Week ID": "={{ $('Loop over each comparison URL').item.json[\"Current Week File ID\"] }}",
"Current Week File ID": "={{ $json.id }}",
"Current Week File Link": "={{ $json.webViewLink }}",
"Previous Week File Link": "={{ $('Loop over each comparison URL').item.json[\"Current Week File Link\"] }}"
},
"schema": [
{
"id": "URL",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "URL",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Previous Week ID",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Previous Week ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Previous Week File Link",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Previous Week File Link",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Current Week File ID",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Current Week File ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Current Week File Link",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Current Week File Link",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Comparison File",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Comparison File",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "row_number",
"type": "string",
"display": true,
"removed": true,
"readOnly": true,
"required": false,
"displayName": "row_number",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"URL"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "update",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1e4oheZjmxb3P7OXGY0uLWZKz2ENcp7XlEpIloOtRHEk/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1e4oheZjmxb3P7OXGY0uLWZKz2ENcp7XlEpIloOtRHEk",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1e4oheZjmxb3P7OXGY0uLWZKz2ENcp7XlEpIloOtRHEk/edit?usp=drivesdk",
"cachedResultName": "BrightData Scraping Comparison"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": "vnXMSwscCP06bp9u",
"name": "Google Sheets account"
}
},
"typeVersion": 4.5
},
{
"id": "32934604-9e72-4f6a-aa55-fc0abff1d85c",
"name": "检查上周文件是否存在",
"type": "n8n-nodes-base.if",
"position": [
1720,
-365
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "d46019c6-ec3b-4e8b-80b2-44ffb2a82e51",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json[\"Previous Week ID\"] }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "301dfc46-f1d4-4b1a-892b-25ecb4495b77",
"name": "下载上周文件",
"type": "n8n-nodes-base.googleDrive",
"position": [
1940,
-365
],
"parameters": {
"fileId": {
"__rl": true,
"mode": "id",
"value": "={{ $json[\"Previous Week ID\"] }}"
},
"options": {},
"operation": "download"
},
"credentials": {
"googleDriveOAuth2Api": {
"id": "ENsCK6J7JBSny3Pv",
"name": "Google Drive account"
}
},
"typeVersion": 3
},
{
"id": "036d00c9-7414-4883-ac56-ef9368195b85",
"name": "将上周文件转换为 JSON",
"type": "n8n-nodes-base.extractFromFile",
"position": [
2160,
-365
],
"parameters": {
"options": {},
"operation": "fromJson"
},
"typeVersion": 1
},
{
"id": "67b8ee0a-2ab0-43cd-b24f-0c8ba6b04cd1",
"name": "设置上周和本周",
"type": "n8n-nodes-base.set",
"position": [
2380,
-365
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "af75985e-4f86-4493-9fbd-0d0c7b383001",
"name": "previous",
"type": "object",
"value": "={{ $json.data[0].output }}"
},
{
"id": "718aeb81-59e9-49f9-aa5b-dc3c194d2efa",
"name": "current",
"type": "object",
"value": "={{ $('Web scraping and data extraction agent').item.json.output }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "40b828f7-08f2-41ea-a6f7-595c5e4ae3e0",
"name": "检查是否为测试模式",
"type": "n8n-nodes-base.if",
"position": [
2600,
-365
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "650a5f5e-3041-4d2f-a891-1d369b09c17a",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $('Loop over each comparison URL').item.json.IsTest }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "af562b9f-3cfc-4715-ae47-779a0554e7d2",
"name": "模拟上周变更",
"type": "n8n-nodes-base.code",
"position": [
2820,
-440
],
"parameters": {
"jsCode": "// Code node in N8n\nconst inputData = $input.first().json;\n\n// Clone the previous data before modifying it\nconst previous = {...inputData.previous};\n\n// Change 1: Increase all numeric prices by $5 in the pricing array\nif (previous?.pricing && Array.isArray(previous.pricing)) {\n previous.pricing.forEach(plan => {\n // Check if price is a numeric string\n if (plan.price && !isNaN(parseFloat(plan.price))) {\n // Add $5 to the price\n plan.price = (parseFloat(plan.price) + 5).toString();\n }\n });\n}\n\n// Change 2: Modify a feature in the first paid plan (if exists)\nif (previous?.pricing && previous.pricing.length > 1) {\n const firstPaidPlan = previous.pricing.find(plan => \n plan.price && plan.price !== \"0\" && !isNaN(parseFloat(plan.price)));\n \n if (firstPaidPlan && Array.isArray(firstPaidPlan.features) && firstPaidPlan.features.length > 1) {\n // Add \"Premium\" prefix to the second feature (if it exists)\n if (firstPaidPlan.features[1]) {\n firstPaidPlan.features[1] = \"Premium \" + firstPaidPlan.features[1];\n }\n }\n}\n\n// Change 3: Update the first FAQ answer (if exists)\nif (previous?.faq && Array.isArray(previous.faq) && previous.faq.length > 0) {\n if (previous.faq[0] && previous.faq[0].answer) {\n // Add a sentence to the end of the answer\n previous.faq[0].answer += \" For more information, contact our sales team.\";\n }\n}\n\n// Return the data with current and modified previous\nreturn [{\n json: {\n current: inputData.current,\n previous: previous\n }\n}];"
},
"typeVersion": 2
},
{
"id": "dd764b6d-d412-44e6-8816-e7b66bbb44c6",
"name": "检测周间变化",
"type": "n8n-nodes-base.code",
"position": [
3040,
-365
],
"parameters": {
"jsCode": "// N8n Code Node to compare previous and current data\n// Input: $input.first().json contains previous and current data objects\n\nfunction detectChanges() {\n const previous = $input.first().json.previous;\n const current = $input.first().json.current;\n \n // Initialize changes object with better structure for Markdown generation\n const changes = {\n detected: false,\n sections: {}, // Organized by section\n summary: {\n totalChanges: 0,\n timestamp: new Date().toISOString()\n }\n };\n \n // Helper function to add a change to the changes object\n function addChange(section, path, changeType, changeData) {\n // Initialize section if it doesn't exist\n if (!changes.sections[section]) {\n changes.sections[section] = {\n name: formatSectionName(section),\n changes: []\n };\n }\n \n // Format display path\n const displayPath = formatPath(path);\n \n // Add change to the section\n changes.sections[section].changes.push({\n displayPath,\n type: changeType,\n ...changeData\n });\n \n // Update global detected flag and counter\n changes.detected = true;\n changes.summary.totalChanges++;\n }\n \n // Helper function to format section name for display\n function formatSectionName(name) {\n // Convert camelCase to Title Case with spaces\n return name.replace(/([A-Z])/g, ' $1')\n .replace(/^./, str => str.toUpperCase())\n .trim();\n }\n \n // Helper function to format path for display\n function formatPath(path) {\n // Format the path elements to be more readable\n return path.replace(/\\[([^\\]]+)\\]/g, ' ($1)');\n }\n \n // Helper function to compare primitive values\n function compareValues(prevVal, currVal, path) {\n if (prevVal !== currVal) {\n const section = path.split('.')[0];\n addChange(section, path, 'changed', {\n old: prevVal,\n new: currVal\n });\n return true;\n }\n return false;\n }\n \n // Helper function to compare arrays\n function compareArrays(prevArr, currArr, path, matchKey = null) {\n let changed = false;\n const section = path.split('.')[0];\n const additions = [];\n const deletions = [];\n \n // Check for deletions\n for (let i = 0; i < prevArr.length; i++) {\n const prevItem = prevArr[i];\n \n if (matchKey && typeof prevItem === 'object' && prevItem !== null) {\n // For arrays of objects with a specified match key (like planName or question)\n const matchFound = currArr.some(currItem => currItem[matchKey] === prevItem[matchKey]);\n if (!matchFound) {\n deletions.push(prevItem);\n changed = true;\n }\n } else {\n // For arrays of primitives\n if (!currArr.includes(prevItem)) {\n deletions.push(prevItem);\n changed = true;\n }\n }\n }\n \n // Check for additions\n for (let i = 0; i < currArr.length; i++) {\n const currItem = currArr[i];\n \n if (matchKey && typeof currItem === 'object' && currItem !== null) {\n // For arrays of objects with a specified match key\n const matchFound = prevArr.some(prevItem => prevItem[matchKey] === currItem[matchKey]);\n if (!matchFound) {\n additions.push(currItem);\n changed = true;\n }\n } else {\n // For arrays of primitives\n if (!prevArr.includes(currItem)) {\n additions.push(currItem);\n changed = true;\n }\n }\n }\n \n // Check for changes in matching objects\n if (matchKey) {\n for (let i = 0; i < currArr.length; i++) {\n const currItem = currArr[i];\n if (typeof currItem === 'object' && currItem !== null) {\n const matchingPrevItem = prevArr.find(prevItem => prevItem[matchKey] === currItem[matchKey]);\n if (matchingPrevItem) {\n // Compare the matching objects recursively\n const itemPath = `${path}[${matchKey}=${currItem[matchKey]}]`;\n compareObjects(matchingPrevItem, currItem, itemPath);\n }\n }\n }\n }\n \n if (changed) {\n addChange(section, path, 'array_changed', {\n additions: additions.length > 0 ? additions : null,\n deletions: deletions.length > 0 ? deletions : null\n });\n }\n \n return changed;\n }\n \n // Helper function to compare objects\n function compareObjects(prevObj, currObj, path = '') {\n if (!prevObj || !currObj) return false;\n \n const allKeys = new Set([...Object.keys(prevObj), ...Object.keys(currObj)]);\n let changed = false;\n \n for (const key of allKeys) {\n // Skip rawMarkdown as requested\n if (key === 'rawMarkdown') continue;\n \n const keyPath = path ? `${path}.${key}` : key;\n const section = keyPath.split('.')[0];\n const prevVal = prevObj[key];\n const currVal = currObj[key];\n \n // Handle missing keys\n if (!(key in prevObj)) {\n addChange(section, keyPath, 'added', {\n value: currVal\n });\n changed = true;\n continue;\n }\n \n if (!(key in currObj)) {\n addChange(section, keyPath, 'removed', {\n value: prevVal\n });\n changed = true;\n continue;\n }\n \n // Compare based on type\n if (Array.isArray(prevVal) && Array.isArray(currVal)) {\n // Special handling for specific array types\n if (key === 'pricing') {\n compareArrays(prevVal, currVal, keyPath, 'planName');\n } else if (key === 'features') {\n compareArrays(prevVal, currVal, keyPath);\n } else if (key === 'faq') {\n compareArrays(prevVal, currVal, keyPath, 'question');\n } else {\n compareArrays(prevVal, currVal, keyPath);\n }\n } else if (\n typeof prevVal === 'object' && prevVal !== null &&\n typeof currVal === 'object' && currVal !== null\n ) {\n compareObjects(prevVal, currVal, keyPath);\n } else {\n compareValues(prevVal, currVal, keyPath);\n }\n }\n \n return changed;\n }\n \n // Start comparison - removed the .output reference\n compareObjects(previous, current);\n \n return [{ json: {previous, current, changes} }];\n}\n\n// Execute the function and return the results\nreturn detectChanges();"
},
"typeVersion": 2
},
{
"id": "e5a8b7c1-53ec-4a26-8791-30db494cde92",
"name": "从检测到的变更生成 Markdown",
"type": "n8n-nodes-base.code",
"position": [
3260,
-365
],
"parameters": {
"jsCode": "// N8n Code Node to generate Markdown from structured changes\n// Input: $input.changes contains the comparison results from the improved comparison node\nfunction generateChangelogMarkdown() {\n // Changed to directly access the changes property from the input\n const changes = $input.first().json.changes;\n \n if (!changes || !changes.detected) {\n return [{ json: { markdown: \"# Changelog\\n\\nNo changes detected.\" }}];\n }\n \n // Start building the markdown\n // Getting the filename from the input for the title\n let fileNameWithoutExt = '';\n try {\n // Try to get the filename from the Loop Over Items node if available\n fileNameWithoutExt = $('Loop over each comparison URL').first().json[\"\"][0];\n } catch (e) {\n // Fallback to a generic title if the loop node isn't available\n fileNameWithoutExt = \"Website\";\n }\n \n let markdown = `# ${fileNameWithoutExt} Changes\\n\\n`;\n \n // Process each section in the changes object\n Object.values(changes.sections).forEach(section => {\n markdown += `## ${section.name}\\n\\n`;\n \n // Process each change in this section\n section.changes.forEach(change => {\n switch (change.type) {\n case 'changed':\n markdown += `- **${change.displayPath}** changed from \\`${formatValue(change.old)}\\` to \\`${formatValue(change.new)}\\`\\n`;\n break;\n \n case 'added':\n markdown += `- **${change.displayPath}** was added with value \\`${formatValue(change.value)}\\`\\n`;\n break;\n \n case 'removed':\n markdown += `- **${change.displayPath}** was removed (previously \\`${formatValue(change.value)}\\`)\\n`;\n break;\n \n case 'array_changed':\n markdown += `- **${change.displayPath}** has changes:\\n`;\n \n // Handle additions\n if (change.additions && change.additions.length > 0) {\n markdown += ` - **Added**:\\n`;\n change.additions.forEach(item => {\n markdown += ` - \\`${formatValue(item)}\\`\\n`;\n });\n }\n \n // Handle deletions\n if (change.deletions && change.deletions.length > 0) {\n markdown += ` - **Removed**:\\n`;\n change.deletions.forEach(item => {\n markdown += ` - \\`${formatValue(item)}\\`\\n`;\n });\n }\n break;\n }\n });\n \n markdown += '\\n';\n });\n \n // Add summary section\n markdown += `## Summary\\n\\n`;\n markdown += `Total changes detected: **${changes.summary.totalChanges}**\\n\\n`;\n markdown += `Generated on: **${new Date(changes.summary.timestamp).toLocaleDateString('en-US', {\n year: 'numeric',\n month: 'long',\n day: 'numeric'\n })}**\\n`;\n \n // Return in the format expected by n8n\n return [{ json: { markdown } }];\n}\n\n// Helper function to format values for markdown display\nfunction formatValue(value) {\n if (value === null || value === undefined) {\n return 'null';\n } else if (typeof value === 'object') {\n if (Array.isArray(value)) {\n // For arrays, simplify to show length\n if (value.length === 0) return '[]';\n if (value.length > 3) {\n return `[Array with ${value.length} items]`;\n }\n // For small arrays, show the items\n return JSON.stringify(value).substring(0, 60) + (JSON.stringify(value).length > 60 ? '...' : '');\n }\n \n // For objects with a name or key identifier, try to use that\n if (value.name) return value.name;\n if (value.title) return value.title;\n if (value.id) return value.id;\n if (value.planName) return value.planName;\n if (value.question) return value.question;\n \n // For other objects, shorten to reasonable length\n const objStr = JSON.stringify(value);\n return objStr.substring(0, 60) + (objStr.length > 60 ? '...' : '');\n } else if (typeof value === 'string') {\n // For strings, add quoting\n return value;\n } else {\n // For other primitives, convert to string\n return String(value);\n }\n}\n\n// Execute the function and return the markdown result\nreturn generateChangelogMarkdown();"
},
"typeVersion": 2
},
{
"id": "16c9f196-6acf-4d3a-946b-727c121b503a",
"name": "将 Markdown 转换为 HTML",
"type": "n8n-nodes-base.markdown",
"position": [
3480,
-365
],
"parameters": {
"mode": "markdownToHtml",
"options": {},
"markdown": "={{ $json.markdown }}"
},
"typeVersion": 1
},
{
"id": "3df4c31f-db47-40a5-9140-d2de7b1fbf2e",
"name": "创建对比文档",
"type": "n8n-nodes-base.googleDocs",
"position": [
3700,
-365
],
"parameters": {
"title": "={{ $('Web scraping and data extraction agent').first().json.output.filename.replace(/\\.[^/.]+$/, '') + '-comparison.md' }}",
"folderId": "default"
},
"credentials": {
"googleDocsOAuth2Api": {
"id": "T3HEUOkeb37yLim7",
"name": "Google Docs account"
}
},
"typeVersion": 2
},
{
"id": "da24c1a3-fa3b-4b06-b5b9-78dd6d872756",
"name": "使用结果更新对比文档",
"type": "n8n-nodes-base.googleDocs",
"position": [
3920,
-365
],
"parameters": {
"actionsUi": {
"actionFields": [
{
"text": "={{ $('Convert Markdown to HTML').item.json.data }}",
"action": "insert"
}
]
},
"operation": "update",
"documentURL": "={{ $json.id }}"
},
"credentials": {
"googleDocsOAuth2Api": {
"id": "T3HEUOkeb37yLim7",
"name": "Google Docs account"
}
},
"typeVersion": 2
},
{
"id": "b532d34b-0ac0-4a00-9949-a9a959e27b31",
"name": "使用对比文件更新对比电子表格",
"type": "n8n-nodes-base.googleSheets",
"position": [
4140,
-365
],
"parameters": {
"columns": {
"value": {
"URL": "={{ $('Loop over each comparison URL').item.json.URL }}",
"Comparison File": "=https://docs.google.com/document/d/{{ $json.documentId }}"
},
"schema": [
{
"id": "URL",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "URL",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Previous Week ID",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Previous Week ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Previous Week File Link",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Previous Week File Link",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Current Week File ID",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Current Week File ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Current Week File Link",
"type": "string",
"display": true,
"removed": true,
"required": false,
"displayName": "Current Week File Link",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Comparison File",
"type": "string",
"display": true,
"required": false,
"displayName": "Comparison File",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "row_number",
"type": "string",
"display": true,
"removed": true,
"readOnly": true,
"required": false,
"displayName": "row_number",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"URL"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "update",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1e4oheZjmxb3P7OXGY0uLWZKz2ENcp7XlEpIloOtRHEk/edit#gid=0",
"cachedResultName": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1e4oheZjmxb3P7OXGY0uLWZKz2ENcp7XlEpIloOtRHEk",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1e4oheZjmxb3P7OXGY0uLWZKz2ENcp7XlEpIloOtRHEk/edit?usp=drivesdk",
"cachedResultName": "BrightData Scraping Comparison"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"id": "vnXMSwscCP06bp9u",
"name": "Google Sheets account"
}
},
"typeVersion": 4.5
},
{
"id": "7ef3ba20-c79e-4d49-8a68-c11d20c77def",
"name": "发送对比结果邮件",
"type": "n8n-nodes-base.gmail",
"position": [
4340,
-260
],
"webhookId": "8d51cb0f-f585-4222-93dd-2fffcead588e",
"parameters": {
"sendTo": "dave@emberautomation.com",
"message": "={{ $('Convert Markdown to HTML').item.json.data }}",
"options": {},
"subject": "={{ $now.format('yyyy-MM-dd') }}: {{ $('Loop over each comparison URL').item.json.URL }} weekly comparison"
},
"credentials": {
"gmailOAuth2": {
"id": "nkZOm8cNEGHWhTE4",
"name": "Gmail account"
}
},
"typeVersion": 2.1
},
{
"id": "a0926fe8-80c2-456b-b9c2-4dc1675c408c",
"name": "便签",
"type": "n8n-nodes-base.stickyNote",
"position": [
-540,
-1340
],
"parameters": {
"width": 760,
"height": 1060,
"content": "# BrightData 每周对比"
},
"typeVersion": 1
},
{
"id": "33c8b369-b6f7-4bb9-87e2-d202d998255d",
"name": "GPT-4.1",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
620,
-145
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "gpt-4.1",
"cachedResultName": "gpt-4.1"
},
"options": {}
},
"credentials": {
"openAiApi": {
"id": "NuECwZyKpEfWhSN1",
"name": "OpenAi account"
}
},
"typeVersion": 1.2
},
{
"id": "700e8bb2-607d-4889-b248-88bf6464c587",
"name": "计划触发器",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-480,
-120
],
"parameters": {
"rule": {
"interval": [
{
"field": "weeks",
"triggerAtDay": [
3
],
"triggerAtHour": 12
}
]
}
},
"typeVersion": 1.2
},
{
"id": "c2919b53-c13f-4033-86f2-3b4e2e28dfa8",
"name": "scrape_as_markdown",
"type": "n8n-nodes-mcp.mcpClientTool",
"position": [
740,
-145
],
"parameters": {
"toolName": "scrape_as_markdown",
"operation": "executeTool",
"toolParameters": "={{ /*n8n-auto-generated-fromAI-override*/ $fromAI('Tool_Parameters', ``, 'json') }}"
},
"credentials": {
"mcpClientApi": {
"id": "mO1My0js13704jqM",
"name": "MCP Client (STDIO) account"
}
},
"typeVersion": 1
},
{
"id": "cbbfbf09-9d2b-4c3b-bcbc-fc43f198f0c0",
"name": "自动修复输出解析器",
"type": "@n8n/n8n-nodes-langchain.outputParserAutofixing",
"position": [
640,
20
],
"parameters": {
"options": {
"prompt": "Instructions:\n--------------\n{instructions}\n--------------\nCompletion:\n--------------\n{completion}\n--------------\n\nAbove, the Completion did not satisfy the constraints given in the Instructions.\nError:\n--------------\n{error}\n--------------\n\nPlease try again. Please only respond with an answer that satisfies the constraints laid out in the Instructions:"
}
},
"typeVersion": 1
},
{
"id": "f3766efe-53a7-4311-9b53-af9f8d11b292",
"name": "便签1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-540,
-260
],
"parameters": {
"color": 2,
"width": 220,
"height": 340,
"content": "## 每周定时运行"
},
"typeVersion": 1
},
{
"id": "93f1107c-8e81-439b-a912-5900e36afd8b",
"name": "便签2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-280,
-260
],
"parameters": {
"color": 3,
"width": 620,
"height": 340,
"content": "## 初始化"
},
"typeVersion": 1
},
{
"id": "f4882880-6ee4-4a44-9967-1be01de7151c",
"name": "便签3",
"type": "n8n-nodes-base.stickyNote",
"position": [
580,
-520
],
"parameters": {
"color": 4,
"width": 400,
"height": 780,
"content": "## AI 抓取"
},
"typeVersion": 1
},
{
"id": "8f6da1fd-c91a-4c99-94f8-b2464a0786be",
"name": "便签4",
"type": "n8n-nodes-base.stickyNote",
"position": [
1020,
-520
],
"parameters": {
"color": 5,
"width": 860,
"height": 400,
"content": "## 处理本周结果"
},
"typeVersion": 1
},
{
"id": "25f758b4-2e11-4cde-bcc3-1a8e53106d29",
"name": "便签5",
"type": "n8n-nodes-base.stickyNote",
"position": [
1900,
-520
],
"parameters": {
"color": 6,
"width": 640,
"height": 400,
"content": "## 处理上周结果"
},
"typeVersion": 1
},
{
"id": "f8d7c276-9540-431d-860d-10e15b769fe4",
"name": "便签6",
"type": "n8n-nodes-base.stickyNote",
"position": [
2560,
-520
],
"parameters": {
"color": 7,
"width": 440,
"height": 400,
"content": "## 模拟"
},
"typeVersion": 1
},
{
"id": "c2dfbe5e-e79e-4cc8-91f2-a61bfef2721e",
"name": "便签7",
"type": "n8n-nodes-base.stickyNote",
"position": [
3020,
-520
],
"parameters": {
"color": 2,
"width": 1260,
"height": 400,
"content": "## 本周与上周对比"
},
"typeVersion": 1
},
{
"id": "a261bcd8-91a1-40f4-928f-19013b68b998",
"name": "将本周 JSON 响应转换为文件",
"type": "n8n-nodes-base.convertToFile",
"position": [
1060,
-365
],
"parameters": {
"options": {},
"operation": "toJson"
},
"typeVersion": 1.1
},
{
"id": "dc49334a-6d00-4476-b84c-7bddfa3c0911",
"name": "便签8",
"type": "n8n-nodes-base.stickyNote",
"position": [
4300,
-520
],
"parameters": {
"width": 380,
"height": 400,
"content": "## 发送对比邮件"
},
"typeVersion": 1
}
],
"active": false,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "3556e422-eddc-40df-8f4f-860d621258d4",
"connections": {
"GPT-4.1": {
"ai_languageModel": [
[
{
"node": "Web scraping and data extraction agent",
"type": "ai_languageModel",
"index": 0
},
{
"node": "Auto-fixing Output Parser",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Set workflow variables",
"type": "main",
"index": 0
}
]
]
},
"Check if test mode": {
"main": [
[
{
"node": "Mock previous week changes",
"type": "main",
"index": 0
}
],
[
{
"node": "Detect changes between weeks",
"type": "main",
"index": 0
}
]
]
},
"scrape_as_markdown": {
"ai_tool": [
[
{
"node": "Web scraping and data extraction agent",
"type": "ai_tool",
"index": 0
}
]
]
},
"Set workflow variables": {
"main": [
[
{
"node": "Read from comparison spreadsheets",
"type": "main",
"index": 0
}
]
]
},
"Convert Markdown to HTML": {
"main": [
[
{
"node": "Create comparison document",
"type": "main",
"index": 0
}
]
]
},
"Structured Output Parser": {
"ai_outputParser": [
[
{
"node": "Auto-fixing Output Parser",
"type": "ai_outputParser",
"index": 0
}
]
]
},
"Auto-fixing Output Parser": {
"ai_outputParser": [
[
{
"node": "Web scraping and data extraction agent",
"type": "ai_outputParser",
"index": 0
}
]
]
},
"Create comparison document": {
"main": [
[
{
"node": "Update comparison document with results",
"type": "main",
"index": 0
}
]
]
},
"Mock previous week changes": {
"main": [
[
{
"node": "Detect changes between weeks",
"type": "main",
"index": 0
}
]
]
},
"Detect changes between weeks": {
"main": [
[
{
"node": "Generate Markdown from detected changes",
"type": "main",
"index": 0
}
]
]
},
"Download previous week's file": {
"main": [
[
{
"node": "Convert previous week's file to JSON",
"type": "main",
"index": 0
}
]
]
},
"Loop over each comparison URL": {
"main": [
[],
[
{
"node": "Web scraping and data extraction agent",
"type": "main",
"index": 0
}
]
]
},
"Upload current week JSON file": {
"main": [
[
{
"node": "Update comparison sheet with current week file data",
"type": "main",
"index": 0
}
]
]
},
"Send email of comparison results": {
"main": [
[
{
"node": "Loop over each comparison URL",
"type": "main",
"index": 0
}
]
]
},
"Read from comparison spreadsheets": {
"main": [
[
{
"node": "Merge workflow variables with Google Sheet data",
"type": "main",
"index": 0
}
]
]
},
"Set previous week and current week": {
"main": [
[
{
"node": "Check if test mode",
"type": "main",
"index": 0
}
]
]
},
"Convert previous week's file to JSON": {
"main": [
[
{
"node": "Set previous week and current week",
"type": "main",
"index": 0
}
]
]
},
"Check presence of previous week's file": {
"main": [
[
{
"node": "Download previous week's file",
"type": "main",
"index": 0
}
],
[
{
"node": "Loop over each comparison URL",
"type": "main",
"index": 0
}
]
]
},
"Web scraping and data extraction agent": {
"main": [
[
{
"node": "Convert current week JSON response to file",
"type": "main",
"index": 0
}
]
]
},
"Generate Markdown from detected changes": {
"main": [
[
{
"node": "Convert Markdown to HTML",
"type": "main",
"index": 0
}
]
]
},
"Update comparison document with results": {
"main": [
[
{
"node": "Update comparison spreadsheet with comparison file",
"type": "main",
"index": 0
}
]
]
},
"Convert current week JSON response to file": {
"main": [
[
{
"node": "Upload current week JSON file",
"type": "main",
"index": 0
}
]
]
},
"Merge workflow variables with Google Sheet data": {
"main": [
[
{
"node": "Loop over each comparison URL",
"type": "main",
"index": 0
}
]
]
},
"Update comparison spreadsheet with comparison file": {
"main": [
[
{
"node": "Send email of comparison results",
"type": "main",
"index": 0
}
]
]
},
"Update comparison sheet with current week file data": {
"main": [
[
{
"node": "Check presence of previous week's file",
"type": "main",
"index": 0
}
]
]
}
}
}常见问题
如何使用这个工作流?
复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。
这个工作流适合什么场景?
高级 - 人工智能, 营销
需要付费吗?
本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。
相关工作流推荐
(Duc)深度研究市场模板
集成PerplexityAI研究和OpenAI内容的多层级WordPress博客生成器
If
Set
Xml
+28
132 节点Daniel Ng
人工智能
在可视化参考库中探索n8n节点
在可视化参考库中探索n8n节点
If
Ftp
Set
+93
113 节点I versus AI
其他
WordPress博客自动化专业版(深度研究)v1
WordPress自动博客专业版 - 含深度研究的内容自动化机器
If
Set
Xml
+24
77 节点Daniel Ng
人工智能
[模板] AI宠物店 v8
🐶 AI宠物店助手 - 集成GPT-4o、Google日历和WhatsApp/Instagram/Facebook
If
N8n
Set
+38
244 节点Amanda Benks
销售
宠物店 4
🐶 宠物店预约 AI 代理
If
Set
Code
+41
187 节点Bruno Dias
人工智能
自动化新闻到简报AI代理v13
AI新闻研究团队:24/7简报自动化,含Perplexity引用
Set
Code
Gmail
+15
37 节点Derek Cheung
产品
工作流信息
难度等级
高级
节点数量35
分类2
节点类型18
作者
Daniel Shashko
@tomaxAI automation specialist and a marketing enthusiast. More than 6 years of experience in SEO/GEO. Senior SEO at Bright Data.
外部链接
在 n8n.io 查看 →
分享此工作流