8
n8n 中文网amn8n.com

创建具有GPT-4o聊天界面和可视化报告的交互式Snowflake数据浏览器

高级

这是一个Internal Wiki, AI Chatbot领域的自动化工作流,包含 28 个节点。主要使用 If, Set, Webhook, Aggregate, Snowflake 等节点。 创建具有GPT-4o聊天界面和可视化报告的交互式Snowflake数据浏览器

前置要求
  • HTTP Webhook 端点(n8n 会自动生成)
  • OpenAI API Key
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "meta": {
    "instanceId": "6a2a7715680b8313f7cb4676321c5baa46680adfb913072f089f2766f42e43bd",
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "695a7e9f-01c6-4290-a085-55099d8db7db",
      "name": "当收到聊天消息时",
      "type": "@n8n/n8n-nodes-langchain.chatTrigger",
      "position": [
        1100,
        -280
      ],
      "webhookId": "8cc040be-d37a-4090-b93e-44603371d13a",
      "parameters": {
        "options": {}
      },
      "typeVersion": 1.1
    },
    {
      "id": "6971de5a-a520-4f74-a5c4-87039b22a15f",
      "name": "AI Agent1",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        1340,
        -280
      ],
      "parameters": {
        "options": {
          "systemMessage": "You are Snowflake SQL assistant.\n\nUse tools to retrieve data from Snowflake and answer user.\n\nIMPORTANT Always check database schema and table definition for preparing SQL query."
        },
        "hasOutputParser": true
      },
      "typeVersion": 2
    },
    {
      "id": "f14d403f-a3d1-4535-aaca-767f9258166c",
      "name": "OpenAI 聊天模型1",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        1140,
        -80
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o-mini"
        },
        "options": {}
      },
      "credentials": {
        "openAiApi": {
          "id": "SphXAX7rlwRLkiox",
          "name": "Test club key"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "7f57e580-ac48-4926-8dda-5ae581ca7345",
      "name": "简单记忆",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "position": [
        1260,
        -80
      ],
      "parameters": {
        "sessionKey": "={{ $('When chat message received').item.json.sessionId }}",
        "sessionIdType": "customKey"
      },
      "typeVersion": 1.3
    },
    {
      "id": "f809bc79-d5ed-466c-82c2-4c298a079f10",
      "name": "数据库架构1",
      "type": "n8n-nodes-base.snowflakeTool",
      "position": [
        1540,
        -80
      ],
      "parameters": {
        "query": "SELECT table_schema, table_name\nFROM information_schema.tables\nWHERE table_schema = 'TPCH_SF1';",
        "operation": "executeQuery",
        "descriptionType": "manual",
        "toolDescription": "Get list of all tables in database"
      },
      "credentials": {
        "snowflake": {
          "id": "YWnLSlN2NAjYvAfU",
          "name": "Snowflake account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "68907f64-aa11-4044-b2ba-d7796fff90f4",
      "name": "获取表定义",
      "type": "n8n-nodes-base.snowflakeTool",
      "position": [
        1660,
        -80
      ],
      "parameters": {
        "query": "SELECT \n    column_name,\n    data_type\n\nFROM \n    SNOWFLAKE_SAMPLE_DATA.information_schema.columns\nWHERE \n    table_name = '{{ $fromAI(\"table_name\") }}'\n    AND table_schema = 'TPCH_SF1'\nORDER BY \n    ordinal_position;\n",
        "operation": "executeQuery",
        "descriptionType": "manual",
        "toolDescription": "Get table definition to find all columns and types."
      },
      "credentials": {
        "snowflake": {
          "id": "YWnLSlN2NAjYvAfU",
          "name": "Snowflake account"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "226b3c2e-7058-476e-a96c-d6cf8bd4f53b",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        1000,
        860
      ],
      "webhookId": "87893585-d157-468d-a9af-7238784e814c",
      "parameters": {
        "path": "87893585-d157-468d-a9af-7238784e814c",
        "options": {},
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "f7abd0a8-8aa4-4aa7-ae24-22d40736c67f",
      "name": "设置HTML",
      "type": "n8n-nodes-base.set",
      "onError": "continueErrorOutput",
      "position": [
        1600,
        860
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "1257d6ca-3c5c-476b-8d26-f8fb84a0c38e",
              "name": "html",
              "type": "string",
              "value": "=<!DOCTYPE html>\n<html>\n<head>\n    <title>Dashboard</title>\n    <style>\n        body { \n            font-family: Arial, sans-serif; \n            padding: 20px;\n            margin: 0;\n        }\n        .container { \n            max-width: 1200px; \n            margin: 0 auto; \n        }\n        table {\n            border-collapse: collapse;\n            width: 100%;\n            margin: 20px 0;\n        }\n        th, td {\n            border: 1px solid #ddd;\n            padding: 8px;\n            text-align: left;\n        }\n        th {\n            background-color: #f2f2f2;\n            cursor: pointer;\n        }\n        th:hover {\n            background-color: #ddd;\n        }\n        .controls {\n            margin: 20px 0;\n            padding: 10px;\n            background: #f8f8f8;\n            border-radius: 4px;\n        }\n        .btn {\n            padding: 8px 16px;\n            margin: 0 5px;\n            cursor: pointer;\n            background: #4CAF50;\n            color: white;\n            border: none;\n            border-radius: 4px;\n        }\n        .btn:hover {\n            background: #45a049;\n        }\n        .tab-container {\n            margin: 20px 0;\n        }\n        .tab-button {\n            padding: 10px 20px;\n            border: none;\n            background: #f2f2f2;\n            cursor: pointer;\n        }\n        .tab-button.active {\n            background: #4CAF50;\n            color: white;\n        }\n        .graph-controls {\n            display: grid;\n            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));\n            gap: 15px;\n            margin: 20px 0;\n            padding: 15px;\n            background: #f8f8f8;\n            border-radius: 4px;\n        }\n        .control-group {\n            display: flex;\n            flex-direction: column;\n            gap: 5px;\n        }\n        select, input {\n            padding: 8px;\n            border: 1px solid #ddd;\n            border-radius: 4px;\n        }\n        #graphContainer {\n            background: white;\n            padding: 20px;\n            border: 1px solid #ddd;\n            border-radius: 4px;\n            margin: 20px 0;\n        }\n/* Sorting icons styles */\nth {\n    position: relative;\n    padding-right: 25px; /* Space for sort icon */\n}\nth:after {\n    content: '↕';\n    position: absolute;\n    right: 8px;\n    color: #999;\n}\nth.sort-asc:after {\n    content: '↑';\n    color: #000;\n}\nth.sort-desc:after {\n    content: '↓';\n    color: #000;\n}\n\n/* Pagination styles */\n.pagination {\n    display: flex;\n    gap: 10px;\n    align-items: center;\n    margin: 20px 0;\n    justify-content: center;\n}\n.pagination button {\n    padding: 5px 10px;\n    border: 1px solid #ddd;\n    background: white;\n    cursor: pointer;\n    border-radius: 4px;\n}\n.pagination button:disabled {\n    background: #f5f5f5;\n    cursor: not-allowed;\n    opacity: 0.5;\n}\n.pagination span {\n    padding: 0 10px;\n}\n.page-size {\n    margin-left: 20px;\n}\n.templates-section {\n    grid-column: 1 / -1;\n    padding: 10px;\n    border-bottom: 1px solid #ddd;\n    margin-bottom: 15px;\n}\n\n.template-buttons {\n    display: flex;\n    gap: 10px;\n    flex-wrap: wrap;\n    margin-top: 10px;\n}\n\n.template-button {\n    padding: 8px 16px;\n    background: #f0f0f0;\n    border: 1px solid #ddd;\n    border-radius: 4px;\n    cursor: pointer;\n    transition: all 0.3s;\n}\n\n.template-button:hover {\n    background: #e0e0e0;\n}\n\n.template-button.active {\n    background: #4CAF50;\n    color: white;\n    border-color: #4CAF50;\n}\n    </style>\n   <!-- React and ReactDOM dependencies -->\n    <script crossorigin src=\"https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js\"></script>\n    <script crossorigin src=\"https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js\"></script>\n    \n\n\n<!-- Add this after other script tags -->\n<script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script>\n\n</head>\n<body>\n    <div class=\"container\">\n        <h1>Dashboard</h1>\n        \n        <div class=\"tab-container\">\n            <button class=\"tab-button active\" onclick=\"switchTab('table')\">Table View</button>\n            <button class=\"tab-button\" onclick=\"switchTab('graph')\">Graph View</button>\n        </div>\n\n        <!-- Table View -->\n        <div id=\"tableView\">\n            <div class=\"controls\">\n                <input type=\"text\" id=\"filterInput\" placeholder=\"Search...\" />\n                <select id=\"filterColumn\">\n                    <option value=\"all\">All Columns</option>\n                </select>\n                <button class=\"btn\" onclick=\"exportToCSV()\">Export to CSV</button>\n            </div>\n            <div id=\"tableContainer\"></div>\n<div class=\"pagination\">\n    <button onclick=\"previousPage()\" id=\"prevBtn\">Previous</button>\n    <span id=\"pageInfo\">Page 1 of 1</span>\n    <button onclick=\"nextPage()\" id=\"nextBtn\">Next</button>\n    <select id=\"pageSize\" onchange=\"changePageSize()\" class=\"page-size\">\n        <option value=\"5\">5 per page</option>\n        <option value=\"10\" selected>10 per page</option>\n        <option value=\"20\">20 per page</option>\n        <option value=\"50\">50 per page</option>\n    </select>\n</div>\n        </div>\n\n        <!-- Graph View -->\n       <div id=\"graphView\" style=\"display: none;\">\n    <div class=\"graph-controls\">\n        <!-- Add templates section -->\n        <div class=\"templates-section\">\n            <h3>Chart Templates</h3>\n            <div id=\"templateButtons\" class=\"template-buttons\">\n                <!-- Template buttons will be added here dynamically -->\n            </div>\n        </div>\n        \n        <!-- Existing controls -->\n        <div class=\"control-group\">\n            <label>X Axis:</label>\n            <select id=\"xAxis\" onchange=\"updateGraph()\"></select>\n        </div>\n        <div class=\"control-group\">\n            <label>Y Axis:</label>\n            <select id=\"yAxis\" onchange=\"updateGraph()\"></select>\n        </div>\n        <div class=\"control-group\">\n            <label>Group By:</label>\n            <select id=\"groupBy\" onchange=\"updateGraph()\"></select>\n        </div>\n        <div class=\"control-group\">\n            <label>Chart Type:</label>\n            <select id=\"chartType\" onchange=\"updateGraph()\">\n    <option value=\"bar\">Bar Chart</option>\n    <option value=\"line\">Line Chart</option>\n    <option value=\"pie\">Pie Chart</option>\n    <option value=\"doughnut\">Doughnut Chart</option>\n</select>\n        </div>\n<!-- Add this in graph-controls div after other controls -->\n<div class=\"control-group\">\n    <label>Aggregation:</label>\n    <select id=\"aggregation\" onchange=\"updateGraph()\">\n        <option value=\"sum\">Sum</option>\n        <option value=\"average\">Average</option>\n        <option value=\"count\">Count</option>\n        <option value=\"min\">Minimum</option>\n        <option value=\"max\">Maximum</option>\n    </select>\n</div>\n    </div>\n    <div id=\"graphContainer\">\n        <canvas id=\"myChart\"></canvas>\n    </div>\n</div>\n\n    <script>\n        // Sample data - sales transactions\n        const data = {{ JSON.stringify($json.data) }};\n        // Column type definitions for formatting\n        function detectColumnTypes(data) {\n    if (!data || data.length === 0) return {};\n    \n    // Get keys from first data item\n    const firstItem = data[0];\n    const columnTypes = {};\n    \n    // Determine type for each column\n    Object.keys(firstItem).forEach(key => {\n        const value = firstItem[key];\n        let type = 'string'; // default type\n        \n        if (value instanceof Date) {\n            type = 'date';\n        } else if (typeof value === 'number') {\n            type = 'number';\n        } else if (typeof value === 'boolean') {\n            type = 'boolean';\n        } else if (typeof value === 'string') {\n            // Check if string is actually a date\n            const dateCheck = new Date(value);\n            if (!isNaN(dateCheck) && value.includes('-')) {\n                type = 'date';\n            } else {\n                type = 'string';\n            }\n        }\n        \n        columnTypes[key] = type;\n    });\n    \n    return columnTypes;\n}\n\n// Remove the hardcoded columnTypes and replace with:\nlet columnTypes = {};\n\n        let filteredData = [...data];\n        let currentPage = 1;\n        let pageSize = 10;\nlet currentSortColumn = null;\nlet sortAscending = true;\n\n\n\nfunction initializeGraphControls() {\n    const templateContainer = document.getElementById('templateButtons');\n    templateContainer.innerHTML = chartTemplates.map((template, index) => `\n        <button class=\"template-button\" onclick=\"applyTemplate(${index})\">\n            ${template.title}\n        </button>\n    `).join('');\n}\n\n    function initialize() {\n    // Detect column types from data\n    columnTypes = detectColumnTypes(data);\n    console.log('Detected columns:', columnTypes);\n    \n    const columns = Object.keys(columnTypes);\n    \n    // Initialize filter dropdown\n    const filterColumn = document.getElementById('filterColumn');\n    filterColumn.innerHTML = '<option value=\"all\">All Columns</option>';\n    columns.forEach(column => {\n        filterColumn.innerHTML += `<option value=\"${column}\">${column}</option>`;\n    });\n\n    // Initialize graph axis selectors\n    const xAxis = document.getElementById('xAxis');\n    const yAxis = document.getElementById('yAxis');\n    // In the initialize function, add empty option for groupBy\nconst groupBy = document.getElementById('groupBy');\ngroupBy.innerHTML = '<option value=\"\">No Grouping</option>';\ncolumns.forEach(column => {\n    groupBy.innerHTML += `<option value=\"${column}\">${column}</option>`;\n});\n    \n    xAxis.innerHTML = '';\n    yAxis.innerHTML = '';\n    \n    columns.forEach(column => {\n        xAxis.innerHTML += `<option value=\"${column}\">${column}</option>`;\n        yAxis.innerHTML += `<option value=\"${column}\">${column}</option>`;\n        groupBy.innerHTML += `<option value=\"${column}\">${column}</option>`;\n    });\n\n    // Set default Y axis to a numeric column\n    const numericColumns = columns.filter(col => columnTypes[col] === 'number');\n    if (numericColumns.length > 0) {\n        yAxis.value = numericColumns[0];\n    }\n\n    setupEventListeners();\n    refreshTable();\n    initializeGraphControls();\n}\n\n        // Format cell values based on type\n        function formatValue(value, type) {\n            if (value === null || value === undefined) return '';\n            \n            switch (type) {\n                case 'date':\n                    return new Date(value).toLocaleDateString();\n                case 'number':\n                    return value.toLocaleString();\n                case 'boolean':\n                    return value ? 'Yes' : 'No';\n                default:\n                    return value.toString();\n            }\n        }\n\n        // Create and update table\n        function createTable() {\n    const table = document.createElement('table');\n    const thead = document.createElement('thead');\n    const tbody = document.createElement('tbody');\n    \n    // Create header row with sorting indicators\n    const headerRow = document.createElement('tr');\n    Object.keys(columnTypes).forEach(column => {\n        const th = document.createElement('th');\n        th.textContent = column.charAt(0).toUpperCase() + column.slice(1);\n        th.onclick = () => sortTable(column);\n        \n        // Add sorting indicators\n        if (column === currentSortColumn) {\n            th.classList.add(sortAscending ? 'sort-asc' : 'sort-desc');\n        }\n        \n        headerRow.appendChild(th);\n    });\n    thead.appendChild(headerRow);\n    \n    // Apply pagination\n    const startIndex = (currentPage - 1) * pageSize;\n    const endIndex = Math.min(startIndex + pageSize, filteredData.length);\n    const paginatedData = filteredData.slice(startIndex, endIndex);\n    \n    // Create data rows\n    paginatedData.forEach(row => {\n        const tr = document.createElement('tr');\n        Object.entries(columnTypes).forEach(([column, type]) => {\n            const td = document.createElement('td');\n            td.textContent = formatValue(row[column], type);\n            if (type === 'boolean') {\n                td.style.color = row[column] ? 'green' : 'red';\n            }\n            tr.appendChild(td);\n        });\n        tbody.appendChild(tr);\n    });\n    \n    table.appendChild(thead);\n    table.appendChild(tbody);\n    updatePagination();\n    return table;\n}\n\n        // Sort table by column\n        // Replace your existing sortTable function\nfunction sortTable(column) {\n    if (currentSortColumn === column) {\n        sortAscending = !sortAscending;\n    } else {\n        currentSortColumn = column;\n        sortAscending = true;\n    }\n    \n    const type = columnTypes[column];\n    filteredData.sort((a, b) => {\n        const valueA = a[column];\n        const valueB = b[column];\n        \n        let comparison = 0;\n        switch (type) {\n            case 'number':\n                comparison = valueA - valueB;\n                break;\n            case 'date':\n                comparison = new Date(valueA) - new Date(valueB);\n                break;\n            case 'boolean':\n                comparison = valueA === valueB ? 0 : valueA ? -1 : 1;\n                break;\n            default:\n                comparison = valueA.toString().localeCompare(valueB.toString());\n        }\n        return sortAscending ? comparison : -comparison;\n    });\n    \n    refreshTable();\n}\n\n        // Filter data\n        function filterData() {\n            const filterValue = document.getElementById('filterInput').value.toLowerCase();\n            const filterColumn = document.getElementById('filterColumn').value;\n            \n            filteredData = data.filter(row => {\n                if (filterColumn === 'all') {\n                    return Object.values(row).some(value => \n                        value.toString().toLowerCase().includes(filterValue)\n                    );\n                } else {\n                    return row[filterColumn].toString().toLowerCase().includes(filterValue);\n                }\n            });\n            \n            refreshTable();\n            if (document.getElementById('graphView').style.display !== 'none') {\n                updateGraph();\n            }\n        }\n\n        // Update graph\n       function updateGraph() {\n    const xAxis = document.getElementById('xAxis').value;\n    const yAxis = document.getElementById('yAxis').value;\n    const groupBy = document.getElementById('groupBy').value;\n    const chartType = document.getElementById('chartType').value;\n    const aggregation = document.getElementById('aggregation').value;\n    \n    // Prepare data\n    const groupedData = {};\n    filteredData.forEach(row => {\n        const groupKey = groupBy ? row[groupBy] : 'All';\n        if (!groupedData[groupKey]) {\n            groupedData[groupKey] = {};\n        }\n        const xValue = formatValue(row[xAxis], columnTypes[xAxis]);\n        if (!groupedData[groupKey][xValue]) {\n            groupedData[groupKey][xValue] = [];\n        }\n        groupedData[groupKey][xValue].push(parseFloat(row[yAxis]) || 0);\n    });\n\n    // Process data with aggregation\n    const labels = [...new Set(filteredData.map(item => formatValue(item[xAxis], columnTypes[xAxis])))];\n    const datasets = Object.entries(groupedData).map(([key, values]) => ({\n        label: key,\n        data: labels.map(label => {\n            const vals = values[label] || [];\n            switch(aggregation) {\n                case 'sum':\n                    return vals.reduce((a, b) => a + b, 0);\n                case 'average':\n                    return vals.length ? vals.reduce((a, b) => a + b, 0) / vals.length : 0;\n                case 'count':\n                    return vals.length;\n                case 'min':\n                    return vals.length ? Math.min(...vals) : 0;\n                case 'max':\n                    return vals.length ? Math.max(...vals) : 0;\n                default:\n                    return vals.reduce((a, b) => a + b, 0);\n            }\n        })\n    }));\n    // Create chart\n    const ctx = document.getElementById('myChart');\n    if (window.currentChart) {\n        window.currentChart.destroy();\n    }\n    \n   // Update the chart configuration in updateGraph function\nwindow.currentChart = new Chart(ctx, {\n    type: chartType,\n    data: {\n        labels: labels,\n        datasets: datasets.map(dataset => ({\n            ...dataset,\n            // Line chart settings\n            borderWidth: 2,\n            tension: 0,  // Remove curve for straight lines\n            pointRadius: 4,  // Larger points\n            pointHoverRadius: 6,\n            \n            // Bar chart settings\n            barPercentage: 0.8,  // Make bars wider (0-1)\n            categoryPercentage: 0.9,  // Space between bar groups\n            \n            // Colors\n            backgroundColor: dataset.backgroundColor || 'rgba(75, 192, 192, 0.6)',\n            borderColor: dataset.borderColor || 'rgba(75, 192, 192, 1)',\n        }))\n    },\n    options: {\n        responsive: true,\n        plugins: {\n            legend: {\n                position: 'top',\n            }\n        },\n        scales: chartType !== 'pie' && chartType !== 'doughnut' ? {\n            y: {\n                beginAtZero: true,\n                ticks: {\n                    font: {\n                        size: 12\n                    }\n                }\n            },\n            x: {\n                ticks: {\n                    font: {\n                        size: 12\n                    }\n                }\n            }\n        } : undefined,\n        // Pie chart size control\n        layout: {\n            padding: 20\n        },\n        aspectRatio: chartType === 'pie' || chartType === 'doughnut' ? 2 : 1.5,\n    }\n});}\n\n        // Export to CSV\n        function exportToCSV() {\n            const headers = Object.keys(columnTypes);\n            const csv = [\n                headers.join(','),\n                ...filteredData.map(row =>\n                    headers.map(header => {\n                        const value = formatValue(row[header], columnTypes[header]);\n                        return `\"${value.toString().replace(/\"/g, '\"\"')}\"`\n                    }).join(',')\n                )\n            ].join('\\n');\n            \n            const blob = new Blob([csv], { type: 'text/csv' });\n            const url = window.URL.createObjectURL(blob);\n            const a = document.createElement('a');\n            a.href = url;\n            a.download = 'export.csv';\n            a.click();\n        }\n\n        // Switch between table and graph views\n        function switchTab(tab) {\n            document.querySelectorAll('.tab-button').forEach(button => {\n                button.classList.remove('active');\n            });\n            document.querySelector(`[onclick=\"switchTab('${tab}')\"]`).classList.add('active');\n            \n            document.getElementById('tableView').style.display = tab === 'table' ? 'block' : 'none';\n            document.getElementById('graphView').style.display = tab === 'graph' ? 'block' : 'none';\n            \n            if (tab === 'graph') {\n                updateGraph();\n            }\n        }\n\n        // Set up event listeners\n\n        // Set up event listeners\nfunction setupEventListeners() {\n    // Filter input listener\n    document.getElementById('filterInput').addEventListener('input', filterData);\n    document.getElementById('filterColumn').addEventListener('change', filterData);\n    \n    // Graph control listeners\n    document.getElementById('xAxis').addEventListener('change', updateGraph);\n    document.getElementById('yAxis').addEventListener('change', updateGraph);\n    document.getElementById('groupBy').addEventListener('change', updateGraph);\n    document.getElementById('chartType').addEventListener('change', updateGraph);\n    document.getElementById('aggregation').addEventListener('change', updateGraph); // Add this line\n}\n\n        // Refresh table display\n        function refreshTable() {\n            const container = document.getElementById('tableContainer');\n            container.innerHTML = '';\n            container.appendChild(createTable());\n        }\n// Add these pagination functions before initialize()\nfunction updatePagination() {\n    const totalPages = Math.ceil(filteredData.length / pageSize);\n    document.getElementById('pageInfo').textContent = `Page ${currentPage} of ${totalPages}`;\n    document.getElementById('prevBtn').disabled = currentPage === 1;\n    document.getElementById('nextBtn').disabled = currentPage === totalPages || totalPages === 0;\n}\n\nfunction previousPage() {\n    if (currentPage > 1) {\n        currentPage--;\n        refreshTable();\n    }\n}\n\nfunction nextPage() {\n    const totalPages = Math.ceil(filteredData.length / pageSize);\n    if (currentPage < totalPages) {\n        currentPage++;\n        refreshTable();\n    }\n}\n\nfunction changePageSize() {\n    pageSize = parseInt(document.getElementById('pageSize').value);\n    currentPage = 1;\n    refreshTable();\n}\n        // Start the application\n        initialize();\n    </script>\n</body>\n</html>"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "e08d0061-a5b9-452d-aae1-dd06c5e76478",
      "name": "响应 Webhook",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        2020,
        860
      ],
      "parameters": {
        "options": {},
        "respondWith": "text",
        "responseBody": "={{ $json.html }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "caec4943-2a9e-4c93-b9f0-46c05e1c977b",
      "name": "Snowflake1",
      "type": "n8n-nodes-base.snowflake",
      "onError": "continueErrorOutput",
      "position": [
        1200,
        860
      ],
      "parameters": {
        "query": "{{ $json.query.sql }}",
        "operation": "executeQuery"
      },
      "credentials": {
        "snowflake": {
          "id": "YWnLSlN2NAjYvAfU",
          "name": "Snowflake account"
        }
      },
      "retryOnFail": false,
      "typeVersion": 1
    },
    {
      "id": "7bf75ddf-38df-454d-908f-32cbeb785464",
      "name": "聚合1",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        1420,
        860
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    },
    {
      "id": "eb82d533-e0a0-4328-a31c-39433cf45740",
      "name": "当由另一个工作流执行时",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "position": [
        1000,
        320
      ],
      "parameters": {
        "workflowInputs": {
          "values": [
            {
              "name": "query"
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "3b69b3eb-7a91-43b3-8839-13b5bbdf3203",
      "name": "检索数据",
      "type": "@n8n/n8n-nodes-langchain.toolWorkflow",
      "position": [
        1400,
        -80
      ],
      "parameters": {
        "workflowId": {
          "__rl": true,
          "mode": "list",
          "value": "kqpZSjy0tzRRY4hH",
          "cachedResultName": "My workflow 41"
        },
        "description": "使用关于数据库架构和表定义的知识生成自定义SQL查询,以提供用户请求所需的响应。",
        "workflowInputs": {
          "value": {
            "query": "={{ $fromAI(\"sql_query\",\"SQL query\") }}"
          },
          "schema": [
            {
              "id": "query",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "query",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "query"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "8968d722-6b26-42d9-b750-0b13c382f7ab",
      "name": "便签",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        940,
        120
      ],
      "parameters": {
        "width": 1140,
        "height": 560,
        "content": "### 工具"
      },
      "typeVersion": 1
    },
    {
      "id": "33544700-b10f-441a-852c-0f985158cb5c",
      "name": "便签1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        940,
        720
      ],
      "parameters": {
        "width": 1360,
        "height": 480,
        "content": "### 报告工作流"
      },
      "typeVersion": 1
    },
    {
      "id": "d279cb96-4fc5-4554-9363-669aea575926",
      "name": "便签2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        940,
        -360
      ],
      "parameters": {
        "width": 900,
        "height": 440,
        "content": "### 代理"
      },
      "typeVersion": 1
    },
    {
      "id": "fc341443-ec9c-4005-8b67-dce59e8b4119",
      "name": "执行SQL",
      "type": "n8n-nodes-base.snowflake",
      "onError": "continueErrorOutput",
      "position": [
        1200,
        320
      ],
      "parameters": {
        "query": "{{ $json.query }}",
        "operation": "executeQuery"
      },
      "credentials": {
        "snowflake": {
          "id": "YWnLSlN2NAjYvAfU",
          "name": "Snowflake account"
        }
      },
      "retryOnFail": false,
      "typeVersion": 1
    },
    {
      "id": "935f1932-e803-4daf-89ae-e3ce9f322962",
      "name": "聚合数据",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        1420,
        260
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData"
      },
      "typeVersion": 1
    },
    {
      "id": "1c7362be-e338-4976-8a18-a40f11e01ef1",
      "name": "如果计数>100",
      "type": "n8n-nodes-base.if",
      "position": [
        1600,
        280
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "5a485a5a-28fb-4914-8fb6-131a159db08d",
              "operator": {
                "type": "array",
                "operation": "lengthGt",
                "rightType": "number"
              },
              "leftValue": "={{ $json.data }}",
              "rightValue": 100
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "36440a5e-d254-4995-be8e-2e8313b8bc29",
      "name": "链接到报告",
      "type": "n8n-nodes-base.set",
      "position": [
        1800,
        200
      ],
      "parameters": {
        "mode": "raw",
        "options": {},
        "jsonOutput": "={\"output\":\"[Link to report](https://n8n.lowcoding.dev/webhook/87893585-d157-468d-a9af-7238784e814c?sql={{ $('When Executed by Another Workflow').item.json.query.urlEncode() }})\"}"
      },
      "typeVersion": 3.4
    },
    {
      "id": "c581ea63-c210-4df9-b029-f58c1c8da75d",
      "name": "返回数据",
      "type": "n8n-nodes-base.set",
      "position": [
        1800,
        360
      ],
      "parameters": {
        "mode": "raw",
        "options": {},
        "jsonOutput": "={{ $json }}"
      },
      "typeVersion": 3.4
    },
    {
      "id": "4fa89cd3-64d8-40cf-87f1-d604aca9c7eb",
      "name": "返回错误",
      "type": "n8n-nodes-base.set",
      "position": [
        1600,
        460
      ],
      "parameters": {
        "mode": "raw",
        "options": {},
        "jsonOutput": "={{ $json }}"
      },
      "typeVersion": 3.4
    },
    {
      "id": "c67e5a20-e116-4955-bc66-fe8b4384b284",
      "name": "错误页面",
      "type": "n8n-nodes-base.set",
      "position": [
        1800,
        1000
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "1257d6ca-3c5c-476b-8d26-f8fb84a0c38e",
              "name": "html",
              "type": "string",
              "value": "=<!DOCTYPE html>\n<html>\n<head>\n    <title>Error Status</title>\n    <style>\n        body { \n            font-family: Arial, sans-serif; \n            padding: 20px;\n            margin: 0;\n            display: flex;\n            min-height: 100vh;\n            align-items: center;\n            justify-content: center;\n            background-color: #f8f9fa;\n        }\n        .container { \n            max-width: 600px;\n            margin: 0 auto;\n            text-align: center;\n            padding: 40px;\n            background: white;\n            border-radius: 8px;\n            box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n        }\n        .error-icon {\n            font-size: 48px;\n            margin-bottom: 20px;\n            color: #dc3545;\n        }\n        .title {\n            color: #dc3545;\n            margin-bottom: 16px;\n            font-size: 24px;\n        }\n        .message {\n            color: #666;\n            margin-bottom: 24px;\n            font-size: 16px;\n            line-height: 1.5;\n        }\n        .btn {\n            padding: 10px 20px;\n            background: #dc3545;\n            color: white;\n            border: none;\n            border-radius: 4px;\n            cursor: pointer;\n            font-size: 16px;\n            text-decoration: none;\n            display: inline-block;\n            margin: 0 8px;\n        }\n        .btn:hover {\n            background: #c82333;\n        }\n        .btn-secondary {\n            background: #6c757d;\n        }\n        .btn-secondary:hover {\n            background: #5a6268;\n        }\n        .details {\n            margin-top: 20px;\n            padding-top: 20px;\n            border-top: 1px solid #eee;\n            color: #888;\n            font-size: 14px;\n        }\n        .error-code {\n            font-family: monospace;\n            background: #f8f9fa;\n            padding: 8px 16px;\n            border-radius: 4px;\n            display: inline-block;\n            margin: 8px 0;\n        }\n    </style>\n</head>\n<body>\n    <div class=\"container\">\n        <div class=\"error-icon\">⚠️</div>\n        <h1 class=\"title\">Error Occurred</h1>\n        <p class=\"message\">An error occurred while preparing the analysis data.</p>\n        <p class=\"message\">Please try again later or contact support if the problem persists.</p>\n        <div class=\"error-code\">Error Code: HTML_PREP_ERROR</div>\n        <div>\n            <a href=\"javascript:window.close();\" class=\"btn\">Close Window</a>\n            <a href=\"javascript:location.reload();\" class=\"btn btn-secondary\">Try Again</a>\n        </div>\n        <div class=\"details\">\n            <p>Analysis ID: <span id=\"analysisId\"></span></p>\n            <p>Time: <span id=\"errorTime\"></span></p>\n        </div>\n    </div>\n\n    <script>\n        // Get URL parameters\n        const urlParams = new URLSearchParams(window.location.search);\n        const analysisId = urlParams.get('analysis_id');\n        \n        // Update details\n        document.getElementById('analysisId').textContent = analysisId || 'N/A';\n        document.getElementById('errorTime').textContent = new Date().toLocaleString();\n    </script>\n</body>\n</html>"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "f614bdcc-e19a-4048-9b13-2adf8b416e56",
      "name": "便签4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1980,
        200
      ],
      "parameters": {
        "color": 5,
        "height": 80,
        "content": "### 替换webhook地址"
      },
      "typeVersion": 1
    },
    {
      "id": "6ac29201-e714-4d00-b9ca-e2063267dcb3",
      "name": "便签5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1360,
        60
      ],
      "parameters": {
        "color": 5,
        "width": 160,
        "height": 80,
        "content": "### 映射此工作流"
      },
      "typeVersion": 1
    },
    {
      "id": "3955749d-31e0-4dc8-9e39-13c1f3de700d",
      "name": "便签6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1540,
        60
      ],
      "parameters": {
        "color": 5,
        "height": 80,
        "content": "### 替换架构和数据库名称"
      },
      "typeVersion": 1
    },
    {
      "id": "b71fbe78-5527-4857-a259-bfa9fc0a0537",
      "name": "便签11",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        260,
        -360
      ],
      "parameters": {
        "color": 7,
        "width": 636.2128494576581,
        "height": 497.1532689930921,
        "content": "![5min Logo](https://res.cloudinary.com/de9"
      },
      "typeVersion": 1
    },
    {
      "id": "a207fdbc-7223-49ed-9798-9d3415634b39",
      "name": "便签12",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        560,
        160
      ],
      "parameters": {
        "color": 7,
        "width": 330.5152611046425,
        "height": 240.6839895136402,
        "content": "### ... or watch set up video [5 min]\n[![Youtube Thumbnail](https://res.cloudinary.com/de9jgixzm/image/upload/nvg4dvgajspjzqudh2wa)](https://youtu.be/r7er-HCRsX4)\n"
      },
      "typeVersion": 1
    }
  ],
  "pinData": {},
  "connections": {
    "Webhook": {
      "main": [
        [
          {
            "node": "Snowflake1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set HTML": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Error page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Agent1": {
      "main": [
        []
      ]
    },
    "Aggregate1": {
      "main": [
        [
          {
            "node": "Set HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "DB Schema1": {
      "ai_tool": [
        [
          {
            "node": "AI Agent1",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Error page": {
      "main": [
        [
          {
            "node": "Respond to Webhook",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Snowflake1": {
      "main": [
        [
          {
            "node": "Aggregate1",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Error page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Execute SQL": {
      "main": [
        [
          {
            "node": "Aggregate Data",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Return Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Count>100": {
      "main": [
        [
          {
            "node": "Link to Report",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Return Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retrieve Data": {
      "ai_tool": [
        [
          {
            "node": "AI Agent1",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "Simple Memory": {
      "ai_memory": [
        [
          {
            "node": "AI Agent1",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Data": {
      "main": [
        [
          {
            "node": "If Count>100",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model1": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent1",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Get table definition": {
      "ai_tool": [
        [
          {
            "node": "AI Agent1",
            "type": "ai_tool",
            "index": 0
          }
        ]
      ]
    },
    "When chat message received": {
      "main": [
        [
          {
            "node": "AI Agent1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Executed by Another Workflow": {
      "main": [
        [
          {
            "node": "Execute SQL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
常见问题

如何使用这个工作流?

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

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

高级 - 内部知识库, AI 聊天机器人

需要付费吗?

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

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

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

作者
Mark Shcherbakov

Mark Shcherbakov

@lowcodingdev

I am a business analyst with a development background, dedicated to helping small businesses and entrepreneurs leverage cloud services for increased efficiency. My expertise lies in automating manual workflows, integrating data from multiple cloud service providers, creating insightful dashboards, and building custom CRM systems. https://www.linkedin.com/in/marklowcoding/

外部链接
在 n8n.io 查看

分享此工作流