8
n8n 中文网amn8n.com

使用 AI 和 Apify 将 LinkedIn 帖子摘要自动发送到 Slack

中级

这是一个AI Summarization, Multimodal AI领域的自动化工作流,包含 14 个节点。主要使用 Code, Cron, Slack, HttpRequest, GoogleSheets 等节点。 使用 AI 和 Apify 将 LinkedIn 帖子摘要自动发送到 Slack

前置要求
  • Slack Bot Token 或 Webhook URL
  • 可能需要目标 API 的认证凭证
  • Google Sheets API 凭证
  • OpenAI API Key
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "meta": {
    "instanceId": "f01290caa6c024522b0ed5bb2d09cea02bb113d8970b898b340ca3d74255326e",
    "templateCredsSetupCompleted": true
  },
  "nodes": [
    {
      "id": "2de83c7c-8f98-4598-ab33-53ac2f05bc17",
      "name": "开始:每周定时任务",
      "type": "n8n-nodes-base.cron",
      "notes": "Runs every Sunday 09:00 Africa/Cairo.",
      "position": [
        400,
        80
      ],
      "parameters": {
        "triggerTimes": {
          "item": [
            {
              "hour": 9
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "fdf4142d-fe4d-44a8-87fb-303e4094349c",
      "name": "从Google Sheets读取个人资料",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        624,
        80
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "id",
          "value": "gid=0"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "__GOOGLE_SHEETS_CREDENTIAL_ID__"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "I0hj4qBrW1yS7OdT",
          "name": "Google Sheets account"
        }
      },
      "typeVersion": 4
    },
    {
      "id": "efa70b5f-002d-4264-8b9d-c639803cf829",
      "name": "Apify:启动爬虫",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1072,
        96
      ],
      "parameters": {
        "url": "https://api.apify.com/v2/acts/apimaestro~linkedin-profile-posts/run-sync-get-dataset-items?token=apify_api_{{YOUR_API_TOKEN}}",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n \"username\": \"{{ $json.profileUrl }}\",\n  \"page_number\": 1,\n  \"limit\": 3,\n  \"maxItems\": 20,\n  \"total_posts\": 3,\n  \"post_type\": \"regular\",\n  \"includePostReactions\": true,\n  \"includePostComments\": false,\n  \"includePostShares\": true,\n  \"extendOutputFunction\": \"async function extendOutputFunction({ item }) { const now=Date.now(); const weekMs=7*24*60*60*1000; const ts=(item?.posted_at?.timestamp)||(item?.postedAt?.timestamp)||0; const postType=item?.post_type||item?.postType||null; if(!ts||now-ts>weekMs||postType!=='regular') return null; const pick=(o,k)=>Object.fromEntries(Object.entries(o||{}).filter(([kk])=>k.includes(kk))); const author=pick(item?.author,['first_name','last_name','headline','username','profile_url','profile_picture']); const stats=pick(item?.stats,['total_reactions','like','support','love','insight','celebrate','comments','reposts']); const media=item?.media?pick(item.media,['type','url','thumbnail','images']):null; const out={ urn:item?.urn||null, full_urn:item?.full_urn||item?.fullUrn||null, posted_at=item?.posted_at||item?.postedAt||null, text:item?.text||null, url=item?.url||null, post_type:postType, author, stats, media, username: author?.profile_url||null }; return out; }\"\n}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4
    },
    {
      "id": "6647dd07-8989-4aee-b5b9-5369db6bfa7c",
      "name": "构建Markdown摘要",
      "type": "n8n-nodes-base.code",
      "position": [
        2144,
        96
      ],
      "parameters": {
        "jsCode": "// Inputs: array of items coming from \"Message a model\"\n// Each item usually looks like { json: { message: { role: 'assistant', content: '...' }, ... } }\n\nconst items = $input.all();\n\n// Helper: pick a grouping key per profile if present\nconst keyOf = (j) =>\n  j.profileUrl ||\n  j.author?.profile_url ||\n  j.author?.username ||\n  j.username ||\n  'Feed';\n\n// Build buckets per profile\nconst buckets = new Map();\nfor (const it of items) {\n  const j = it.json ?? {};\n  const key = keyOf(j);\n  const arr = buckets.get(key) || [];\n  arr.push(j);\n  buckets.set(key, arr);\n}\n\n// Compose markdown\nconst today = new Date().toISOString().slice(0, 10);\nlet md = `# LinkedIn Digest (${today})\\n\\n`;\n\nfor (const [profile, arr] of buckets.entries()) {\n  md += `## ${profile}\\n\\n`;\n  arr.forEach((j, idx) => {\n    const content =\n      j.message?.content ?? j.content ?? j.text ?? '(no content)';\n    const url =\n      j.url || j.post?.url || j.source_url || j.link || null;\n\n    md += `**${idx + 1}.** ${content}\\n`;\n    if (url) md += `\\n[Link](${url})\\n`;\n    md += `\\n---\\n`;\n  });\n  md += `\\n`;\n}\n\n// Slack hard limit ~4000 chars for a single text message.\n// Trim if needed to be safe.\nconst MAX = 3800;\nif (md.length > MAX) {\n  md = md.slice(0, MAX - 50) + '\\n\\n…_truncated_';\n}\n\n// Return ONE item that downstream nodes can use.\n// Slack node: set Message Text = {{ $json.text }}  (or {{ $json.markdown }})\nreturn [\n  {\n    json: {\n      markdown: md,\n      text: md,\n    },\n  },\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "e63e2244-0dfa-4e54-8bac-9abf356138b1",
      "name": "批处理",
      "type": "n8n-nodes-base.code",
      "position": [
        1248,
        96
      ],
      "parameters": {
        "jsCode": "const posts = items.map(item => item.json);\n\n// find min and max date in this batch\nconst dates = posts.map(p => new Date(p.posted_at?.date));\nconst minDate = new Date(Math.min(...dates));\nconst maxDate = new Date(Math.max(...dates));\n\nfunction formatDate(d) {\n  return d.toISOString().split(\"T\")[0]; // YYYY-MM-DD\n}\n\n// group by author username\nconst grouped = {};\nfor (const post of posts) {\n  const author = `${post.author?.first_name || \"\"} ${post.author?.last_name || \"\"}`.trim() || \"Unknown\";\n  if (!grouped[author]) grouped[author] = [];\n  grouped[author].push(post);\n}\n\nlet digest = `## LinkedIn Digest (${formatDate(minDate)} → ${formatDate(maxDate)})\\n\\n`;\n\nfor (const [author, authorPosts] of Object.entries(grouped)) {\n  digest += `### ${author}\\n`;\n  for (const post of authorPosts) {\n    const date = post.posted_at?.date || \"Unknown date\";\n    const text = (post.text || \"\").slice(0, 200).replace(/\\n+/g, \" \"); // keep short snippet\n    const reactions = post.stats?.total_reactions || 0;\n    const comments = post.stats?.comments || 0;\n    const reposts = post.stats?.reposts || 0;\n    digest += `- (${date}) \"${text}...\"\\n  👍 ${reactions} | 💬 ${comments} | 🔁 ${reposts} | [Link](${post.url})\\n`;\n  }\n  digest += \"\\n\";\n}\n\nreturn [{ json: { digest } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "2e0f532a-c8ad-4d71-840e-e2176a43a70b",
      "name": "代码",
      "type": "n8n-nodes-base.code",
      "position": [
        2496,
        96
      ],
      "parameters": {
        "jsCode": "// Input: one item with json.markdown (or json.text)\n// Output: N items -> { text, part, total } for Slack\n\nconst MAX = 35000; // safe margin under Slack's ~40k limit\nconst SEP = '\\n---\\n';\n\nconst md = $input.first().json.markdown ?? $input.first().json.text ?? '';\nif (!md) return [{ json: { text: '(empty digest)' } }];\n\n// Split on section separators, then pack sections into <= MAX chunks\nconst sections = md.split(SEP);\nconst chunks = [];\nlet buf = '';\n\nfunction pushBuf() {\n  if (buf.trim()) chunks.push(buf.trim());\n  buf = '';\n}\n\nfor (const s of sections) {\n  const candidate = buf ? `${buf}${SEP}${s}` : s;\n  if (candidate.length <= MAX) {\n    buf = candidate;\n  } else {\n    // flush what we have\n    pushBuf();\n    // if a single section is still too long, split it on newlines\n    if (s.length > MAX) {\n      let start = 0;\n      while (start < s.length) {\n        let end = Math.min(start + MAX, s.length);\n        const nl = s.lastIndexOf('\\n', end);\n        if (nl > start + 1000) end = nl; // try not to cut mid‑line\n        chunks.push(s.slice(start, end).trim());\n        start = end;\n      }\n    } else {\n      buf = s; // start new buffer with this section\n    }\n  }\n}\npushBuf();\n\nreturn chunks.map((c, i) => ({\n  json: { text: c, part: i + 1, total: chunks.length }\n}));\n"
      },
      "typeVersion": 2
    },
    {
      "id": "7da7a9ec-c541-4556-bf86-9360eae28e37",
      "name": "LinkedIn摘要",
      "type": "n8n-nodes-base.slack",
      "position": [
        2752,
        96
      ],
      "webhookId": "18881790-2665-4d76-83f5-35e6b0e30946",
      "parameters": {
        "text": "=**LinkedIn Digest (part {{$json.part}}/{{$json.total}})**\n\n{{$json.text}}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{TARGET_SLACK_CHANNEL}}"
        },
        "otherOptions": {
          "unfurl_links": "={{ false }}",
          "includeLinkToWorkflow": "={{ false }}"
        }
      },
      "credentials": {
        "slackApi": {
          "id": "SLACK_ID",
          "name": "Your_Slack_HQ"
        }
      },
      "executeOnce": true,
      "typeVersion": 2.3
    },
    {
      "id": "9103c0a5-8a0a-4f55-93f7-920ec2b363fd",
      "name": "Threads消息传递",
      "type": "n8n-nodes-base.slack",
      "position": [
        3168,
        96
      ],
      "webhookId": "52de7e8a-c716-4079-bacd-3ac514a93364",
      "parameters": {
        "text": "={{ $json[\"linksText\"] }}\n",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "={{TARGET_SLACK_CHANNEL}}"
        },
        "otherOptions": {
          "mrkdwn": true,
          "thread_ts": {
            "replyValues": {
              "thread_ts": "={{ $('LinkedIn Digest').item.json.message.ts }}"
            }
          },
          "sendAsUser": "TARGET_USER",
          "includeLinkToWorkflow": "={{ false }}"
        }
      },
      "credentials": {
        "slackApi": {
          "id": "SLACK_ID",
          "name": "MY_SLACK"
        }
      },
      "executeOnce": false,
      "typeVersion": 2.3,
      "alwaysOutputData": false
    },
    {
      "id": "ee8150a4-5ad6-4984-b1cf-3f6630c5e654",
      "name": "源链接",
      "type": "n8n-nodes-base.code",
      "position": [
        2960,
        96
      ],
      "parameters": {
        "jsCode": "// --- CONFIG ---\n// Change this to the exact node name that has the LinkedIn objects:\nconst SOURCE_NODE = 'Apify: Start Scraper';   // e.g. 'API Request' if that’s your node’s name\n\n// --- READ ITEMS SAFELY ---\nconst items = $(SOURCE_NODE).all(); // requires “Run Once for All Items”\n\n// Your API payload shows each item has a .url field.\n// If sometimes it’s nested, add more fallbacks in the mapper.\nconst links = items\n  .map(i => i.json?.url || i.json?.article?.url || i.json?.reshared_post?.url)\n  .filter(Boolean);\n\n// Build Slack-friendly list\nconst linksText = '*Sources for today’s digest:*\\n' +\n  links.map((u, i) => `${i + 1}. <${u}|Post ${i + 1}>`).join('\\n');\n\n// Output a single item for the Slack Threads node\nreturn [{ json: { linksText } }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "3709705b-f03c-4e2c-84fb-3433ec4ec030",
      "name": "遍历项目1循环",
      "type": "n8n-nodes-base.splitInBatches",
      "notes": "You can customize the batch size according to how many posts you want to go through each run of the loop",
      "position": [
        832,
        80
      ],
      "parameters": {
        "options": {},
        "batchSize": 5
      },
      "notesInFlow": true,
      "typeVersion": 3
    },
    {
      "id": "1fb5cf18-b44d-40ac-bba3-5db186fc7587",
      "name": "消息模型",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        1808,
        96
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-5-mini",
          "cachedResultName": "GPT-5-MINI"
        },
        "options": {},
        "messages": {
          "values": [
            {
              "content": "=## LinkedIn Digest ({{ $json.date || \"This Week\" }})\n\nINPUT:\n{{ $json.text }}\n\nTASK:\nSummarize the INPUT per the rules in the system message.\n"
            },
            {
              "role": "system",
              "content": "=You are a professional LinkedIn digest summarizer. \nYour goal is to take raw LinkedIn posts and turn them into a short, structured daily digest. \nConstraints:\n- Each post must be summarized in **max 2–3 bullet points**. \n- Each bullet point must be **≤15 words**. \n- Always include the author’s name in **bold** at the top of each post summary. \n- Do not include hashtags, links, emojis, or filler text. \n- The entire digest must stay under **500 words total**. \n- Keep summaries actionable and focused on insights, not fluff.\nFormat:\n## LinkedIn Digest ({{ $json.date || $json.today }})\n\n**[Author Name]**\n- Bullet\n- Bullet\n\n**[Next Author]**\n- Bullet\n- Bullet\n"
            },
            {
              "content": "If a post has only a link or no clear content, \nexplicitly say \"Shared only a link (no text)\" instead of \"Content unspecified.\""
            }
          ]
        }
      },
      "credentials": {
        "openAiApi": {
          "id": "OPENAI_ID",
          "name": "YOUR_OpenAI"
        }
      },
      "typeVersion": 1.8
    },
    {
      "id": "08242d63-3788-4c6e-a728-8b6063eeeef7",
      "name": "去除Markdown",
      "type": "n8n-nodes-base.code",
      "position": [
        1408,
        96
      ],
      "parameters": {
        "jsCode": "return items.map(item => {\n  let digest = item.json.digest;\n\n  // Remove markdown headers\n  digest = digest.replace(/^#+\\s?/gm, \"\");\n\n  // Replace markdown links [text](url) → just \"text\"\n  digest = digest.replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, \"$1\");\n\n  // Remove \"(Link: …)\" remnants\n  digest = digest.replace(/\\(Link:[^)]+\\)/g, \"\");\n\n  // Unescape quotes \\\" → \"\n  digest = digest.replace(/\\\\\"/g, '\"');\n\n  // Convert \\n to real newlines\n  digest = digest.replace(/\\\\n/g, \"\\n\");\n\n  // Collapse multiple newlines\n  digest = digest.replace(/\\n{2,}/g, \"\\n\");\n\n  // Trim leading/trailing spaces\n  digest = digest.trim();\n\n  return {\n    json: {\n      text: digest\n    }\n  };\n});\n"
      },
      "typeVersion": 2
    },
    {
      "id": "e60efb9d-3c38-4f63-a90f-410f4b09d1cf",
      "name": "日期提取",
      "type": "n8n-nodes-base.code",
      "position": [
        1616,
        96
      ],
      "parameters": {
        "jsCode": "return items.map(item => {\n  const text = item.json.text;\n  const match = text.match(/LinkedIn Digest \\((.*?)\\)/);\n  return {\n    json: {\n      date: match ? match[1] : new Date().toISOString().split(\"T\")[0],\n      text: text.replace(/LinkedIn Digest.*?\\n/, \"\") // strip old header\n    }\n  }\n});\n"
      },
      "typeVersion": 2
    },
    {
      "id": "3bec6bd4-f2c5-464a-82a3-7b76952d5f59",
      "name": "便签",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -656,
        -448
      ],
      "parameters": {
        "width": 944,
        "height": 1536,
        "content": "# 精简版LinkedIn摘要工作流"
      },
      "typeVersion": 1
    }
  ],
  "pinData": {},
  "connections": {
    "Code": {
      "main": [
        [
          {
            "node": "LinkedIn Digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Batch": {
      "main": [
        [
          {
            "node": "Strip Markdown",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Date Extract": {
      "main": [
        [
          {
            "node": "Message a model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Source Links": {
      "main": [
        [
          {
            "node": "Threads Messaging",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Strip Markdown": {
      "main": [
        [
          {
            "node": "Date Extract",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "LinkedIn Digest": {
      "main": [
        [
          {
            "node": "Source Links",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Message a model": {
      "main": [
        [
          {
            "node": "Build Markdown Digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items1": {
      "main": [
        [],
        [
          {
            "node": "Apify: Start Scraper",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Threads Messaging": {
      "main": [
        [
          {
            "node": "Loop Over Items1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Start: Weekly Cron": {
      "main": [
        [
          {
            "node": "Read Profiles from Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Apify: Start Scraper": {
      "main": [
        [
          {
            "node": "Batch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Markdown Digest": {
      "main": [
        [
          {
            "node": "Code",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Profiles from Google Sheets": {
      "main": [
        [
          {
            "node": "Loop Over Items1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
常见问题

如何使用这个工作流?

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

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

中级 - AI 摘要总结, 多模态 AI

需要付费吗?

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

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

适合有一定经验的用户,包含 6-15 个节点的中等复杂度工作流

作者
Ziad Adel

Ziad Adel

@ziadadel

AI Automation Growth Partner with 5 years of experience in the tech and hyper growing startups industry

外部链接
在 n8n.io 查看

分享此工作流