X(原 Twitter)是 AI、开发工具和科技行业信息传播速度最快的平台之一。模型发布、开源项目、产品更新和开发者讨论,往往会先在 X 出现。但持续运营一个 X 账号并不轻松:每天都要选题、压缩内容、准备图片、控制字符数量,再选择合适时间发布。
这篇文章介绍如何使用 n8n + AI 智能体 + Data Table + X API 搭建一套自动内容发布工作流。AI 智能体负责生成内容和封面,n8n 负责读取待发布文章、整理成 X 帖子、上传图片、定时调用 X API,并记录接口返回的 Tweet ID 和文章链接。
完整流程:AI 联网获取选题 → AI 生成文章与短内容 → 自动生成封面 → Data Table 保存状态 → n8n 定时读取 → OAuth 上传图片 → X API 发布帖子 → 返回 Tweet ID。
一、这套 X 自动发布工作流能做什么
- 每天按照指定时间自动启动,也支持手动测试。
- 从
content_articlesData Table 读取已经准备好的文章。 - 只选择文章状态和图片状态都为
ready的记录。 - 清理 Markdown、排版提示和不适合 X 展示的内容。
- 自动把标题、摘要和推广链接压缩到 280 个字符以内。
- 读取文章封面文件,并上传到 X 媒体接口。
- 将文字和图片组合成一条 X 帖子。
- 发布成功后取得 Tweet ID,并生成公开访问链接。
当前工作流一次最多读取 8 篇满足条件的文章,按照 Data Table 中的顺序处理。实际生产环境可以把数量调整为 1 至 3 条,避免短时间连续发送大量内容。
二、AI 智能体与 X 发布工作流如何配合
需要说明的是,X 发布工作流本身主要负责“读取、格式化、上传和发布”。真正的 AI 内容生成位于它的上游。也就是说,完整系统由多个工作流协作,而不是把所有逻辑都塞进一个巨大画布。
AI 新闻智能体(联网搜索)
↓
horizon_summaries Data Table
↓
AI 生产文章
↓
content_articles Data Table
↓
AI 生成封面并写入 image_path
↓
推送 X 平台文章(10:30 定时)
↓
X Media Upload + POST /2/tweets
新闻智能体会先从可信来源整理选题,文章工作流使用大模型扩展正文,生图工作流为文章生成封面。等 article_status=ready 且 image_status=ready 后,X 工作流才开始处理。微信公众号内容生产部分可以参考站内的 n8n + GPT 公众号自动化教程。
三、用 Data Table 连接 AI 内容和 X 发布
X 工作流读取的核心数据表是 content_articles。文章生成、生图、微信草稿和 X 发布都可以围绕这张表协作。当前工作流主要使用以下字段:
article_key:文章唯一标识。title:文章标题,也是 X 帖子的标题部分。markdown:AI 生成的完整正文。image_path:本地封面图片路径。article_status:文章是否已经准备完成。image_status:封面是否已经生成完成。error:保存处理失败原因。
工作流只读取 article_status=ready 和 image_status=ready 的记录。这样即使文章已经写完但图片仍在生成,也不会提前发布一条缺少封面的内容。
四、X 工作流的 10 个节点
1. 手动触发与定时触发
工作流同时保留 Manual Trigger 和 Schedule Trigger。手动触发用于配置 OAuth、检查图片路径和测试接口;定时触发默认安排在每天 10:30。正式启用前,应先完整执行一次手动测试。
2. 读取待发布文章
“读取文章”节点从 content_articles 获取状态符合要求的记录,并限制单次处理数量。这里不直接读取 Markdown 文件,而是以 Data Table 作为稳定的数据中心。
3. 清理和格式化文章
“文章格式处理”节点会删除排版建议、配图提示和多余标记,识别标题、段落、列表和代码块,同时校验文章标题、正文、唯一标识与图片路径是否完整。
4. 准备 280 字符帖子
“准备 X 平台文章数据”节点从完整正文中提取纯文本,删除 Markdown 符号和代码块,然后组合标题、摘要与链接。节点会根据剩余字符动态截断正文,最终把帖子控制在 280 个字符以内。
5. 读取并上传图片
“读取 X 平台图片”节点根据 image_path 读取封面二进制文件,随后通过 OAuth 1.0a 凭证上传图片。当前项目工作流使用 https://upload.twitter.com/1.1/media/upload.json,而 X 最新官方文档同时提供 X API v2 Media Upload。部署时应根据自己的 X API 权限和套餐确认可用端点。
6. 创建 X 帖子
图片上传成功后,接口会返回 media_id。工作流再调用官方 POST /2/tweets 接口,把 tweet_text 和图片编号组合成帖子。成功响应会包含新帖子的 ID。
7. 整理发布结果
最后一个节点会校验接口是否返回 Tweet ID,并生成 x_status、x_tweet_id 和 x_url。当前版本只把结果保存在本次 n8n 执行输出中;正式长期运行时,建议再增加一个 Data Table Update 节点,将结果写回文章记录。
五、为什么必须配置用户身份 OAuth
普通 Bearer Token 通常代表应用本身,不能直接代替某个用户发布帖子。自动发帖需要用户身份授权。X 官方说明,OAuth 1.0a User Context 可以让开发者应用代表已授权账号执行操作;创建帖子也需要有效的用户访问令牌。
当前工作流的图片上传和帖子创建节点都使用 OAuth 1.0a,因此需要在 X Developer Console 创建 App,并准备以下信息:
- API Key,也称 Consumer Key。
- API Key Secret,也称 Consumer Secret。
- Access Token。
- Access Token Secret。
- 具备 Read and Write 权限的用户授权。
在 n8n 中创建 X OAuth API 或通用 OAuth 1.0a 凭证,并让“上传图片到 X 平台”和“推送 X 平台文章”两个 HTTP Request 节点使用同一份凭证。OAuth 1.0a 的请求需要 HMAC-SHA1 签名,建议交给 n8n 凭证系统处理,不要在 Code 节点中自行拼接签名。X 官方的 OAuth 1.0a 文档列出了完整的 Key、Token 和签名关系。
六、定时发布和防止重复推送
Schedule Trigger 默认在每天 10:30 启动,但真正上线前不能只打开 Active。必须先解决重复发布问题。当前 content_articles 表还没有 X 专用状态字段,而且工作流最后也没有 Data Table Update 节点。如果直接每天定时读取相同的 ready 文章,存在重复发送的风险。
推荐在 Data Table 中增加以下字段:
x_status:pending、posted 或 failed。x_tweet_id:X 返回的帖子 ID。x_url:帖子公开链接。x_posted_at:实际发布时间。
随后将读取条件增加为 x_status=pending,并在发帖成功后写回 posted。这样即使 n8n 重启或定时任务重复触发,也不会再次发布已经完成的文章。
七、正式启用前的完整检查清单
- n8n 容器健康,
/healthz返回正常。 - X Developer App 已经创建并开启 Read and Write 权限。
- OAuth 1.0a 用户授权已经完成,不再出现 “There is no request token”。
- 账号具备调用创建帖子和媒体上传接口所需的 API 权限或额度。
- 两处 X HTTP 节点使用同一份 OAuth 凭证。
content_articles中存在一条文章和图片都为 ready 的测试记录。- 封面图片路径可以从 n8n 容器内部读取。
- 手动执行成功并返回真实 Tweet ID。
- 增加 X 状态回写和防重复条件后,再启用 Schedule Trigger。
八、当前项目的实际验证状态
当前边界:工作流 JSON、节点连接、Data Table 读取、字符整理、媒体上传请求和创建帖子请求已经建立;但工作流目前仍为停用状态,没有配置完成的 OAuth 1.0a 凭证,也没有真实成功发布记录。因此本文介绍的是已经落地的工作流结构与部署方法,不代表当前账号已经完成端到端自动发帖。
要把它变成真正无人值守的定时发布系统,至少还需要完成 OAuth 用户授权、确认 X API 权限或额度、增加发布状态回写,并使用真实文章进行一次手动执行。只有拿到 X 返回的 Tweet ID,并在目标账号页面看到帖子,才算完成最终验证。
九、常见故障排查
OAuth 页面提示没有 Request Token
这通常说明 n8n 尚未成功取得临时 Request Token,或者 Callback URL、Consumer Key、Consumer Secret、应用权限配置不一致。重新检查 X Developer App 的回调地址和 n8n OAuth 凭证,不要把 OAuth 2.0 Bearer Token 填到 OAuth 1.0a 节点。
图片上传没有返回 media_id
检查图片路径是否能从 n8n 容器内部访问、二进制字段名称是否为 data、文件格式和大小是否符合接口要求,同时确认当前账号是否仍能使用工作流配置的媒体上传端点。
创建帖子返回 401 或 403
401 通常与签名、Token 或时间偏差有关;403 更常见于应用没有写入权限、用户未授权、接口套餐不支持或者账号受限。修改权限后应重新生成或重新授权 Access Token。
工作流每天重复发布同一篇文章
这是因为当前读取条件只检查文章和图片状态,没有检查 X 发布状态。增加 x_status 字段、成功回写节点和 pending 过滤条件即可。
十、安全和运营建议
- API Key、Secret、Access Token 和 Token Secret 只能保存在 n8n Credential 中。
- 不要把真实密钥写入工作流 JSON、GitHub、文章代码块或执行日志。
- 工作流导入后默认保持停用,完成手动验证后再启用定时器。
- 限制每天发布数量,避免连续发送相似内容。
- AI 生成内容发布前应检查事实、链接、敏感表达和版权风险。
- 定期检查 X Developer Console 的权限、额度和失败响应。
十一、适合哪些使用场景
这套方案适合 AI 科技资讯账号、开发者个人品牌、开源项目更新、企业产品动态、自媒体矩阵和需要跨平台分发内容的团队。上游 AI 智能体负责持续生成素材,下游 n8n 工作流根据不同平台要求改写格式,就可以把同一条内容分别投放到微信公众号、X、WordPress 或 Telegram。
十二、总结
n8n + AI 智能体的价值,不只是自动调用一次 X API,而是建立一条可以观察、重试、扩展和维护的内容生产流水线。AI 负责选题、写作和封面,Data Table 负责状态,n8n 负责定时编排,X API 负责最终发布。
真正投入生产前,需要完成 OAuth、接口权限、状态回写和真实发帖验证。完成这些步骤后,系统才能从“可导入的工作流”升级为稳定运行的自动定时发布服务。
附录:X 自动发布工作流与 Data Table Schema
下面是当前项目中“推送X平台文章”工作流的脱敏导出代码。真实凭证 ID、Data Table ID 和运行数据均已移除或替换为 REPLACE_WITH_*。导入后必须重新选择 Data Table 与 OAuth 1.0a 凭证,并保持工作流停用,直到手动测试成功。
推送 X 平台文章工作流 JSON(点击展开代码)
{
"name": "推送X平台文章",
"nodes": [
{
"parameters": {},
"id": "245b3889-7484-40a1-840e-85f620528695",
"name": "手动触发",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
2176,
-224
]
},
{
"parameters": {
"operation": "get",
"dataTableId": {
"__rl": true,
"value": "REPLACE_WITH_CONTENT_ARTICLES_TABLE_ID",
"mode": "list",
"cachedResultName": "content_articles"
},
"matchType": "allConditions",
"filters": {
"conditions": [
{
"keyName": "article_status",
"keyValue": "ready"
},
{
"keyName": "image_status",
"keyValue": "ready"
}
]
},
"limit": 8,
"orderBy": true,
"orderByDirection": "ASC"
},
"id": "72bd7d25-7805-45a7-8683-d914c84af06b",
"name": "读取文章",
"type": "n8n-nodes-base.dataTable",
"typeVersion": 1.1,
"position": [
1936,
32
]
},
{
"parameters": {
"jsCode": "\nconst allItems = $input.all();\nconst results = [];\nconst failed = [];\n\nfor (let i = 0; i < allItems.length; i++) {\n try {\n let content = String(allItems[i].json.markdown || '').trim();\n const articleKey = String(allItems[i].json.article_key || '').trim();\n const imagePath = String(allItems[i].json.image_path || '').trim();\n const fileName = articleKey ? articleKey + '.md' : 'article.md';\n let title = String(allItems[i].json.title || '').trim().slice(0, 64);\n\n if (!content || !articleKey || !title || !imagePath) {\n throw new Error('Data Table article is missing markdown, article_key, title, or image_path');\n }\n\n const isLayoutTipHeading = (line) => {\n const normalized = line\n .trim()\n .replace(/^[-*#>\\s]+/, '')\n .replace(/^\\d+[.)、]\\s*/, '')\n .replace(/\\*\\*/g, '')\n .replace(/[<《【\\[]/g, '')\n .replace(/[>》】\\]]/g, '');\n return /^(公众号)?(排版提示建议|排版提示|排版建议|排版贴士)([::].*)?$/.test(normalized);\n };\n\n const rawLines = content.split('\\n');\n const tipLineIdx = rawLines.findIndex(isLayoutTipHeading);\n const layoutTipMarker = /(排版提示|排版建议|排版贴士|配图位置建议|配图插入位置参考|重点内容标注建议|文末引导语|图片来源建议)/;\n const markerLineIdx = rawLines.findIndex((line) => layoutTipMarker.test(line));\n const cutLineIdx = [tipLineIdx, markerLineIdx].filter((idx) => idx > -1).sort((a, b) => a - b)[0] ?? -1;\n if (cutLineIdx > -1) {\n let beforeLines = rawLines.slice(0, cutLineIdx);\n while (beforeLines.length > 0) {\n const last = beforeLines[beforeLines.length - 1].trim();\n if (last === '---' || last === '') beforeLines.pop();\n else break;\n }\n content = beforeLines.join('\\n').trimEnd();\n }\n\n const codeBlocks = [];\n content = content.replace(/```[\\s\\S]*?```/g, (match) => {\n codeBlocks.push(match);\n return `__CODE_BLOCK_${codeBlocks.length - 1}__`;\n });\n\n let lines = content.split('\\n');\n let blocks = [];\n let currentParagraph = [];\n let firstNonEmpty = true;\n\n const flushParagraph = () => {\n if (currentParagraph.length > 0) {\n blocks.push({ type: 'paragraph', text: currentParagraph.join(' ').replace(/\\s+/g, ' ').trim() });\n currentParagraph = [];\n }\n };\n\n for (let idx = 0; idx < lines.length; idx++) {\n const trimmed = lines[idx].trim();\n\n if (/^---+\\s*$/.test(trimmed)) {\n flushParagraph();\n continue;\n }\n\n if (firstNonEmpty && trimmed.length > 0) {\n firstNonEmpty = false;\n continue;\n }\n\n if (/^#{1,6}\\s+/.test(trimmed)) {\n flushParagraph();\n blocks.push({\n type: 'subtitle',\n text: trimmed.replace(/^#{1,6}\\s+/, '').replace(/\\*\\*/g, '').trim()\n });\n continue;\n }\n\n if (/^[-*]\\s+/.test(trimmed)) {\n flushParagraph();\n blocks.push({ type: 'list', text: trimmed.replace(/^[-*]\\s+/, '') });\n continue;\n }\n\n if (trimmed === '') {\n flushParagraph();\n continue;\n }\n\n if (/^__CODE_BLOCK_\\d+__$/.test(trimmed)) {\n flushParagraph();\n const idxNum = parseInt(trimmed.match(/\\d+/)[0]);\n blocks.push({ type: 'code', text: codeBlocks[idxNum] });\n continue;\n }\n\n currentParagraph.push(trimmed);\n }\n flushParagraph();\n\n blocks = blocks.map((block) => {\n if (block.type !== 'paragraph') return block;\n\n const text = block.text;\n\n if (/^第[一二三四五六七八九十0-9]+[个类章节部分]/.test(text.trim())) {\n return block;\n }\n\n const match = text.match(/^([^\\s。!?!?]{2,25}?[::??])\\s*(.*)$/s);\n if (match && match[1].length <= 25 && match[1].length >= 4) {\n if (/^[一二三四五六七八九十0-9]+类[::]$/.test(match[1])) {\n return block;\n }\n const subtitleText = match[1].trim();\n const rest = match[2].trim();\n if (rest.length > 0) {\n return [\n { type: 'subtitle', text: subtitleText },\n { type: 'paragraph', text: rest }\n ];\n } else {\n return [{ type: 'subtitle', text: subtitleText }];\n }\n }\n return block;\n }).flat();\n\n blocks = (() => {\n const result = [];\n for (let j = 0; j < blocks.length; j++) {\n const cur = blocks[j];\n const next = blocks[j + 1];\n if (\n cur.type === 'subtitle' &&\n next && next.type === 'paragraph' &&\n next.text.length <= 12 &&\n !/[。!!.,,]/.test(next.text)\n ) {\n result.push({ type: 'subtitle', text: cur.text + next.text });\n j++;\n } else {\n result.push(cur);\n }\n }\n return result;\n })();\n\n let finalBlocks = [];\n for (let j = 0; j < blocks.length; j++) {\n const cur = blocks[j];\n const prev = finalBlocks[finalBlocks.length - 1];\n\n if (cur.type !== 'paragraph') {\n finalBlocks.push(cur);\n continue;\n }\n\n const isVeryShort = cur.text.length < 15;\n if (prev && prev.type === 'paragraph' && cur.text.length < 80 && !cur.text.endsWith(':') && !cur.text.endsWith(':') && !cur.text.endsWith('?') && !cur.text.endsWith('?')) {\n prev.text = prev.text + ' ' + cur.text;\n continue;\n }\n if (prev && prev.type === 'paragraph' && isVeryShort) {\n prev.text = prev.text + cur.text;\n continue;\n }\n\n finalBlocks.push(cur);\n }\n\n const renderParagraph = (text) => {\n text = text.replace(/__CODE_BLOCK_(\\d+)__/g, (m, idx) => codeBlocks[parseInt(idx)] || '');\n return text.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');\n };\n\n let html = '';\n for (const block of finalBlocks) {\n if (block.type === 'subtitle') {\n html += '<p style=\"font-weight:bold;font-size:16px;margin:30px 0 10px 0;line-height:1.6;color:#333;\">' + escapeHtml(block.text.replace(/\\*\\*/g, '').trim()) + '</p>\\n';\n } else if (block.type === 'list') {\n const itemText = renderParagraph(block.text);\n html += '<p style=\"text-indent:0;margin:5px 0 5px 18px;line-height:1.8;font-size:16px;color:#333;\">\\u00b7 ' + itemText + '</p>\\n';\n } else if (block.type === 'code') {\n html += '<pre style=\"background:#f5f5f5;padding:12px;border-radius:4px;overflow-x:auto;font-size:14px;line-height:1.5;margin:15px 0;\">' + escapeHtml(block.text) + '</pre>\\n';\n } else if (block.type === 'paragraph') {\n const paraText = renderParagraph(block.text);\n html += '<p style=\"text-indent:0;margin:15px 0;line-height:1.8;font-size:16px;color:#333;\">' + paraText + '</p>\\n';\n }\n }\n\n if (!html.trim()) {\n failed.push({ fileName, error: 'Empty content after formatting' });\n continue;\n }\n\n const assertions = {\n hasFontSize16: /font-size:16px/.test(html),\n hasMargin30: /margin:30px 0 10px 0/.test(html),\n hasBoldWeight: /font-weight:bold/.test(html),\n hasColor333: /color:#333/.test(html),\n hasSubtitles: (html.match(/font-weight:bold[^\"]*\">[^<]+/g) || []).length >= 1,\n noLayoutTipResidue: !/(排版提示|排版建议|排版贴士|配图插入位置参考|重点内容标注建议)/.test(html)\n };\n const failed_asserts = Object.entries(assertions).filter(([k, v]) => !v).map(([k]) => k);\n if (failed_asserts.length > 0) {\n failed.push({ fileName, error: 'Assertion failed: ' + failed_asserts.join(', ') });\n continue;\n }\n\n const htmlContent = '<div style=\"font-size:16px;color:#333;\">\\n' + html + '</div>';\n results.push({\n json: {\n content: htmlContent,\n title,\n fileName,\n itemType: 'article',\n formatted: true,\n article_key: articleKey,\n image_path: imagePath\n },\n pairedItem: { item: i }\n });\n } catch (err) {\n failed.push({ fileName: allItems[i].json.fileName || `item_${i}`, error: err.message, stack: err.stack });\n }\n}\n\nfunction escapeHtml(text) {\n return String(text)\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"');\n}\n\nif (failed.length > 0) {\n console.log('[format-v3.1] Failed items:', JSON.stringify(failed));\n throw new Error('Some articles failed to format: ' + JSON.stringify(failed));\n}\n\nif (results.length === 0) {\n throw new Error('All articles failed to format: ' + JSON.stringify(failed));\n}\n\nreturn results;\n"
},
"id": "464c788f-8a69-489c-983d-b5b5dfa05f40",
"name": "文章格式处理",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [
2144,
32
]
},
{
"parameters": {
"jsCode": "const items = $input.all();\nconst output = [];\n\nfor (const item of items) {\n const json = item.json || {};\n const articleKey = String(json.article_key || '').trim();\n const title = String(json.title || '').trim();\n const imagePath = String(json.image_path || '').trim();\n const markdown = String(json.markdown || '').trim();\n if (!articleKey || !title || !imagePath || !markdown) {\n throw new Error('文章缺少 article_key、title、image_path 或 markdown: ' + articleKey);\n }\n\n const clean = markdown\n .replace(/排版提示[\\s\\S]*$/i, '')\n .replace(new RegExp('\\x60\\x60\\x60[\\s\\S]*?\\x60\\x60\\x60', 'g'), '')\n .replace(/[#>*_\\-]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n const link = 'https://macosabc.com/token';\n const prefix = title + '\\n\\n';\n const available = Math.max(0, 280 - prefix.length - link.length - 2);\n const excerpt = clean.slice(0, available).trim();\n const text = (prefix + excerpt + (clean.length > available ? '…' : '') + '\\n\\n' + link).slice(0, 280);\n\n output.push({\n json: { article_key: articleKey, title, image_path: imagePath, tweet_text: text },\n });\n}\n\nif (output.length === 0) throw new Error('没有找到待推送到 X 的文章。');\nreturn output;"
},
"id": "54aa1a86-db11-442e-b474-643a65dbbbca",
"name": "准备X平台文章数据",
"type": "n8n-nodes-base.code",
"position": [
2656,
32
],
"typeVersion": 2
},
{
"parameters": {
"fileSelector": "={{ $json.image_path }}",
"options": {}
},
"id": "b61f10d8-def8-4570-b25e-faae42b45129",
"name": "读取X平台图片",
"type": "n8n-nodes-base.readWriteFile",
"position": [
2896,
32
],
"typeVersion": 1.1
},
{
"parameters": {
"method": "POST",
"url": "https://upload.twitter.com/1.1/media/upload.json",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "oAuth1Api",
"sendBody": true,
"contentType": "multipart-form-data",
"bodyParameters": {
"parameters": [
{
"parameterType": "formBinaryData",
"name": "media",
"inputDataFieldName": "data"
}
]
},
"options": {
"response": {
"response": {
"responseFormat": "json"
}
}
}
},
"id": "93e2234e-3e77-4ca1-b6e4-b020134a051d",
"name": "上传图片到X平台",
"type": "n8n-nodes-base.httpRequest",
"position": [
3136,
32
],
"typeVersion": 4.2
},
{
"parameters": {
"jsCode": "const uploads = $input.all();\nconst source = $('准备X平台文章数据').all();\nreturn uploads.map((upload, index) => {\n const article = source[index]?.json || {};\n const mediaId = String(upload.json?.media_id || '').trim();\n if (!mediaId) throw new Error('X 图片上传没有返回 media_id: ' + (article.article_key || index));\n return { json: { ...article, media_id: mediaId } };\n});"
},
"id": "6abe347c-0b3d-4d43-a8fd-39d1921d69b5",
"name": "准备X平台推文",
"type": "n8n-nodes-base.code",
"position": [
3376,
32
],
"typeVersion": 2
},
{
"parameters": {
"method": "POST",
"url": "https://api.x.com/2/tweets",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "oAuth1Api",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ text: $json.tweet_text, media: { media_ids: [$json.media_id] } }) }}",
"options": {
"response": {
"response": {
"responseFormat": "json"
}
}
}
},
"id": "3ef0556c-8be1-4d86-ba41-81fbd4c92949",
"name": "推送X平台文章",
"type": "n8n-nodes-base.httpRequest",
"position": [
3616,
32
],
"typeVersion": 4.2
},
{
"parameters": {
"jsCode": "const responses = $input.all();\nconst source = $('准备X平台推文').all();\nreturn responses.map((item, index) => {\n const article = source[index]?.json || {};\n const response = item.json || {};\n const tweetId = String(response.data?.id || response.id || '').trim();\n if (!tweetId) throw new Error('X 发文接口没有返回推文 ID: ' + (article.article_key || index));\n return { json: {\n article_key: article.article_key,\n title: article.title,\n x_status: 'posted',\n x_tweet_id: tweetId,\n x_url: 'https://x.com/i/web/status/' + tweetId,\n error: '',\n } };\n});"
},
"id": "85255f2c-738e-4285-90e6-933e829d70ec",
"name": "记录X平台推送结果",
"type": "n8n-nodes-base.code",
"position": [
3856,
32
],
"typeVersion": 2
},
{
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 10,
"triggerAtMinute": 30
}
]
}
},
"id": "81174ae9-6959-4625-aaee-9858c26bbe79",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
2176,
288
],
"typeVersion": 1.3,
"disabled": true
}
],
"connections": {
"手动触发": {
"main": [
[
{
"node": "读取文章",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "读取文章",
"type": "main",
"index": 0
}
]
]
},
"读取文章": {
"main": [
[
{
"node": "文章格式处理",
"type": "main",
"index": 0
}
]
]
},
"文章格式处理": {
"main": [
[
{
"node": "准备X平台文章数据",
"type": "main",
"index": 0
}
]
]
},
"准备X平台文章数据": {
"main": [
[
{
"node": "读取X平台图片",
"type": "main",
"index": 0
}
]
]
},
"读取X平台图片": {
"main": [
[
{
"node": "上传图片到X平台",
"type": "main",
"index": 0
}
]
]
},
"上传图片到X平台": {
"main": [
[
{
"node": "准备X平台推文",
"type": "main",
"index": 0
}
]
]
},
"准备X平台推文": {
"main": [
[
{
"node": "推送X平台文章",
"type": "main",
"index": 0
}
]
]
},
"推送X平台文章": {
"main": [
[
{
"node": "记录X平台推送结果",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"binaryMode": "separate"
},
"active": false,
"pinData": {},
"tags": []
}
content_articles Data Table Schema(点击展开代码)
{
"export_type": "n8n_data_table_schema",
"schema_version": 1,
"includes_rows": false,
"table": {
"name": "content_articles",
"table_id_placeholder": "REPLACE_WITH_CONTENT_ARTICLES_TABLE_ID",
"required_columns": [
{
"index": 0,
"name": "article_key",
"type": "string"
},
{
"index": 1,
"name": "source_run_id",
"type": "string"
},
{
"index": 2,
"name": "summary_date",
"type": "string"
},
{
"index": 3,
"name": "title",
"type": "string"
},
{
"index": 4,
"name": "markdown",
"type": "string"
},
{
"index": 5,
"name": "article_status",
"type": "string"
},
{
"index": 6,
"name": "image_path",
"type": "string"
},
{
"index": 7,
"name": "image_status",
"type": "string"
},
{
"index": 8,
"name": "wechat_status",
"type": "string"
},
{
"index": 9,
"name": "wechat_media_id",
"type": "string"
},
{
"index": 10,
"name": "error",
"type": "string"
}
],
"recommended_x_tracking_columns": [
{
"name": "x_status",
"type": "string",
"example": "pending | posted | failed"
},
{
"name": "x_tweet_id",
"type": "string"
},
{
"name": "x_url",
"type": "string"
},
{
"name": "x_posted_at",
"type": "string"
}
]
},
"note": "当前工作流只在执行输出中生成 X 发布结果。正式启用定时任务前,建议增加 Data Table Update 节点并使用 recommended_x_tracking_columns 防止重复发布。"
}





















暂无评论内容