使用 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)可能需要您自行付费。
相关工作流推荐
每日 WhatsApp 群组智能分析:GPT-4.1 分析与语音消息转录
每日 WhatsApp 群组智能分析:GPT-4.1 分析与语音消息转录
If
Set
Code
+20
52 节点Daniel Lianes
杂项
Instagram影响者交付物和合同合规性自动化
使用Claude AI和Slack提醒自动化Instagram影响者合同合规性
If
Set
Code
+8
26 节点Oneclick AI Squad
社交媒体
自定义冷邮件破冰内容生成器
通过Apollo线索抓取和GPT-4.1自动化个性化冷邮件
Set
Code
Slack
+7
24 节点Richard Besier
潜在客户开发
使用 OpenAI、QuickChart 和 Google Drive 将电子表格数据转换为智能图表
使用 OpenAI、QuickChart 和 Google Drive 将电子表格数据转换为智能图表
If
Set
Code
+18
82 节点LeeWei
内容创作
使用Google Sheets和Slack的自动化任务提醒与每日总结
使用Google Sheets和Slack的自动化任务提醒与每日总结
If
Cron
Slack
+2
9 节点Ziad Adel
个人效率
新闻自动收集器 → Google表格
使用NewsAPI、OpenAI和Google表格收集并总结多语言新闻
If
Set
Code
+6
19 节点Supira Inc.
AI 摘要总结
工作流信息
难度等级
中级
节点数量14
分类2
节点类型8
作者
Ziad Adel
@ziadadelAI Automation Growth Partner with 5 years of experience in the tech and hyper growing startups industry
外部链接
在 n8n.io 查看 →
分享此工作流