n8n工作流-自动生成推送微信公众号文章

n8n工作流-自动生成推送微信公众号文章

Horizon:https://github.com/Thysrael/Horizon
n8n: https://github.com/n8n-io/n8n
本项目用到的AItoken免费调用使用推荐站:https://token.macosabc.com
查看成品公众号文章:科特超算

n8n工作流代码:可直接复制导入

以下为工作流核心参考代码,如需交流沟通定制开发 可联系站长(页脚/微信号HFWeChat888)

第一步:生产文章

{
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 9,
              "triggerAtMinute": 20
            }
          ]
        }
      },
      "id": "fadc2da2-23ea-4a7e-99ed-b34728653d27",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        112
      ]
    },
    {
      "parameters": {
        "fileSelector": "=/home/node/.n8n-files/horizon-archive/horizon-{{ $now.format(\"yyyy-MM-dd\") }}-zh.md",
        "options": {}
      },
      "id": "7061b1fd-eb6c-42a5-a44a-c7024a8a80d6",
      "name": "读取Horizon摘要",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1.1,
      "position": [
        224,
        112
      ]
    },
    {
      "parameters": {
        "jsCode": "const buf = await this.helpers.getBinaryDataBuffer(0, 'data');\nconst text = buf.toString('utf-8');\nconst lines = text.split('\\n');\nconst sections = [];\nlet cur = null;\nfor (const line of lines) {\n  if (line.match(/^## /)) {\n    if (cur) sections.push(cur);\n    cur = { title: line.replace(/^## /, '').trim(), content: [] };\n  } else if (cur) {\n    cur.content.push(line);\n  }\n}\nif (cur) sections.push(cur);\n\nconst items = [];\nfor (const s of sections) {\n  const content = s.content.join('\\n').trim();\n  if (content.length > 100) {\n    items.push({ json: { title: s.title, content, url: '', score: 8, source: 'Horizon' } });\n  }\n}\n\nfor (let i = items.length - 1; i > 0; i--) {\n  const j = Math.floor(Math.random() * (i + 1));\n  [items[i], items[j]] = [items[j], items[i]];\n}\nreturn items.slice(0, 2);"
      },
      "id": "96bf958c-7693-4719-a1ee-b140105fd943",
      "name": "解析Horizon",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        448,
        112
      ]
    },
    {
      "parameters": {
        "promptType": "define",
        "text": "=标题:{{ $json.title }}\\n链接:{{ $json.url }}\\n评分:{{ $json.score }}/10\\n\\n原文内容:\\n{{ $json.content }}",
        "options": {
          "systemMessage": "将以下新闻内容扩展改写为一篇完整的公众号文章:\n\n- 首行是文章标题(不加#号),吸引眼球,控制在20字以内\n- 正文用自然段落,中文流畅,结构清晰\n- 适当补充背景知识让读者更好理解\n- 去除原文中的来源链接和地址,不要出现链接\n- 不要有任何解释或提示语,只输出文章内容\n- 适合直接发布\n\n基于你提供的新闻链接及对应内容,完成一篇符合微信公众号发布标准的完整文章文案创作,具体要求如下:\n1. 提炼1个吸睛、符合微信公众号传播逻辑的主标题,同时为正文所有核心段落提炼精准的段落小标题,所有段落正文围绕新闻核心信息展开创作,完整覆盖新闻的关键事实、核心亮点与延伸价值\n2. 全程采用轻松活泼、愉快有趣的口语化文案语气,避免生硬的新闻播报感,加入符合年轻读者阅读习惯的网感表达,增强文案的可读性与传播性\n3. 严格符合微信公众号常规文章的字数要求,总字数控制在1500-2500字区间,适配主流公众号读者的阅读时长,既不内容单薄也不因篇幅过长导致读者流失\n4. 正文所有段落(包括主标题与引言段、各段落小标题与对应正文段之间)都单独占用一行,每两个段落之间保留一行空行隔开,排版逻辑清晰,符合公众号读者的竖屏阅读体验\n5. 完成文案创作后,同步补充适配公众号的排版提示,包括重点内容的标注建议、合适的配图插入位置参考,确保文案可直接用于公众号的发布排版\n6. 要文章大标题分明,段落小标题与上一个段落有两行的分隔行,段落要完整一段的完整起来,不要一小段一小段文字内容。"
        }
      },
      "id": "fda08604-42ab-441c-8e94-b1c19767ee52",
      "name": "AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "typeVersion": 3.1,
      "position": [
        672,
        112
      ]
    },
    {
      "parameters": {
        "options": {}
      },
      "id": "96e21a9a-49e7-49ea-86d2-69f681432d19",
      "name": "DeepSeek Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatDeepSeek",
      "typeVersion": 1,
      "position": [
        688,
        336
      ],
      "credentials": {
        "deepSeekApi": {
          "id": "tBxpiw3zeuEDtoQe",
          "name": "DeepSeek account"
        }
      }
    },
    {
      "parameters": {
        "sessionIdType": "customKey",
        "sessionKey": "={{ $now.format(\"yyyy-MM-dd_HHmmss\") }}_horizon-article-gen"
      },
      "id": "56c02c56-811b-4d17-a62c-46ee0d05e8e3",
      "name": "Simple Memory",
      "type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
      "typeVersion": 1.4,
      "position": [
        816,
        336
      ]
    },
    {
      "parameters": {
        "operation": "toText",
        "sourceProperty": "output",
        "options": {
          "encoding": "utf8",
          "fileName": "={{ ($json.output || \"\").split(\"\\n\")[0].replace(/[\\\\\\/*:?\"<>|]/g, \"\").trim().slice(0, 40) || \"article\" }}.md"
        }
      },
      "id": "2460b517-4adb-46dd-b17c-84932c821d1b",
      "name": "转成文件",
      "type": "n8n-nodes-base.convertToFile",
      "typeVersion": 1.1,
      "position": [
        1024,
        112
      ]
    },
    {
      "parameters": {
        "operation": "write",
        "fileName": "=/home/node/.n8n-files/articles/{{ $now.format(\"yyyy-MM-dd_HHmmss\") }}_{{ $position }}.md",
        "options": {}
      },
      "id": "cce744f3-6b29-49cc-90cd-ffc03950a624",
      "name": "存储文章",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1.1,
      "position": [
        1248,
        112
      ]
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "读取Horizon摘要",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "读取Horizon摘要": {
      "main": [
        [
          {
            "node": "解析Horizon",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "解析Horizon": {
      "main": [
        [
          {
            "node": "AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Agent": {
      "main": [
        [
          {
            "node": "转成文件",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "DeepSeek Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Simple Memory": {
      "ai_memory": [
        [
          {
            "node": "AI Agent",
            "type": "ai_memory",
            "index": 0
          }
        ]
      ]
    },
    "转成文件": {
      "main": [
        [
          {
            "node": "存储文章",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "存储文章": {
      "main": [
        []
      ]
    }
  },
  "pinData": {},
  "meta": {
    "aiBuilderAssisted": true,
    "builderVariant": "mcp",
    "instanceId": "f42815087320c088ecb6cbaef3ecc986b9f5c4b8f6e81b3273a6fc429b262686"
  }
}

第二步:生图

{
  "nodes": [
    {
      "parameters": {},
      "id": "659eeea3-26d7-4551-b675-0cdd3a632bd1",
      "name": "手动触发",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "fileSelector": "/home/node/.n8n-files/articles/*.md",
        "options": {}
      },
      "id": "fa66fa18-a691-448a-a5d2-e2349e06c0fc",
      "name": "读取文章",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1.1,
      "position": [
        224,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const allItems = $input.all();\nconst results = [];\n\nfor (let i = 0; i < allItems.length; i++) {\n  const buf = await this.helpers.getBinaryDataBuffer(i, 'data');\n  let content = buf.toString('utf-8');\n  const fileName = allItems[i].json.fileName || 'article';\n\n  let title = fileName.replace(/\\.md$/, '');\n  let firstLine = '';\n  const contentLines = content.split('\\n');\n  for (const line of contentLines) {\n    const trimmed = line.trim();\n    if (trimmed.length > 0) {\n      title = trimmed;\n      firstLine = trimmed;\n      break;\n    }\n  }\n\n  const keywords = [];\n  const bodyLines = contentLines.slice(1, 20).join(' ');\n  const techTerms = bodyLines.match(/[A-Z][a-zA-Z]+(?:\\s+[A-Z][a-zA-Z]+)*/g);\n  if (techTerms) {\n    const unique = [...new Set(techTerms.map(t => t.trim()).filter(t => t.length > 2))];\n    keywords.push(...unique.slice(0, 5));\n  }\n\n  const imageFileName = fileName.replace(/\\.md$/, '.jpg');\n\n  results.push({\n    json: {\n      title,\n      fileName,\n      imageFileName,\n      keywords: keywords.join(', ')\n    },\n    pairedItem: { item: i }\n  });\n}\n\nreturn results;"
      },
      "id": "de9e8a21-28c4-44e5-9f94-0bc880357241",
      "name": "提取标题",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        448,
        0
      ]
    },
    {
      "parameters": {
        "resource": "image",
        "modelId": {
          "__rl": true,
          "mode": "id",
          "value": "gpt-image-2"
        },
        "prompt": "={{ $json.title }} I need a cover image for a WeChat official account article:\n\n1. Size: 2.35:1\n\n2. Style: pixel art; colors: rich and saturated; avoid excessive use of purple or blue, but some blue and purple are still necessary.\n\n\n",
        "options": {
          "size": "=1536x1024"
        }
      },
      "id": "a4d7c87f-c80b-4d65-97e0-d517742b6e32",
      "name": "生成封面图",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 2.3,
      "position": [
        672,
        0
      ],
      "retryOnFail": false,
      "maxTries": 3,
      "waitBetweenTries": 5000,
      "credentials": {
        "openAiApi": {
          "id": "T7mabTt0ed2hjw22",
          "name": "OpenAI account"
        }
      }
    },
    {
      "parameters": {
        "operation": "crop",
        "width": 1536,
        "height": 1024,
        "options": {
          "fileName": "={{ $(\"提取标题\").item.json.imageFileName }}",
          "format": "jpeg",
          "quality": 92
        }
      },
      "id": "f55d0203-b3f2-480e-a770-97cfa9acca82",
      "name": "转换JPG",
      "type": "n8n-nodes-base.editImage",
      "typeVersion": 1,
      "position": [
        896,
        0
      ]
    },
    {
      "parameters": {
        "operation": "write",
        "fileName": "=/home/node/.n8n-files/lmg/{{ $(\"提取标题\").item.json.imageFileName }}",
        "options": {}
      },
      "id": "e92b5483-ecb7-41e7-bf9f-565e52cbdea9",
      "name": "保存图片",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1.1,
      "position": [
        1120,
        0
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 10,
              "triggerAtMinute": 20
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        0,
        144
      ],
      "id": "2b25349f-cf92-49f4-94d6-8523f2558100",
      "name": "Schedule Trigger"
    }
  ],
  "connections": {
    "手动触发": {
      "main": [
        [
          {
            "node": "读取文章",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "读取文章": {
      "main": [
        [
          {
            "node": "提取标题",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "提取标题": {
      "main": [
        [
          {
            "node": "生成封面图",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "生成封面图": {
      "main": [
        [
          {
            "node": "转换JPG",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "转换JPG": {
      "main": [
        [
          {
            "node": "保存图片",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "读取文章",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {
    "手动触发": [
      {}
    ]
  },
  "meta": {
    "aiBuilderAssisted": true,
    "builderVariant": "mcp",
    "instanceId": "f42815087320c088ecb6cbaef3ecc986b9f5c4b8f6e81b3273a6fc429b262686"
  }
}

第三步:合并上传

{
  "nodes": [
    {
      "parameters": {},
      "id": "dc6b0ca1-a01a-4186-9b8c-b357e9fd4cf9",
      "name": "手动触发",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        128,
        160
      ]
    },
    {
      "parameters": {
        "fileSelector": "/home/node/.n8n-files/articles/*.md",
        "options": {}
      },
      "id": "f0a8a362-5db4-4cf5-85a4-fd0e6b5d7ace",
      "name": "读取文章",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1.1,
      "position": [
        448,
        224
      ]
    },
    {
      "parameters": {
        "jsCode": "const allItems = $input.all();\nconst results = [];\nconst tailSvg = `<section style=\"display:inline-block;width:100%;vertical-align:top;line-height:0;overflow:hidden;\">\n  <svg\n    style=\"background-size:100%;background-repeat:no-repeat;display:block;margin-top:-1px;transform:scale(1);pointer-events:none;background-image:url("https://mmecoa.qpic.cn/mmecoa_png/s0D2micbQKRM0dt70EZP2H0S5dIOrEQvuarNziakPpmiab3eVBarCH5GLAlBgC2MFCez3sM3MxN1pYj7BEtUd9obF3mVDvmTbfibUAS02L3c71I/640?wx_fmt=png&from=appmsg");\"\n    viewBox=\"0 0 1080 2844\"\n    width=\"100%\"\n    data-lazy-bgimg=\"https://mmecoa.qpic.cn/mmecoa_png/s0D2micbQKRM0dt70EZP2H0S5dIOrEQvuarNziakPpmiab3eVBarCH5GLAlBgC2MFCez3sM3MxN1pYj7BEtUd9obF3mVDvmTbfibUAS02L3c71I/640?wx_fmt=png&from=appmsg\"\n    role=\"img\"\n    aria-label=\"插图\"\n    data-fail=\"0\">\n    <g label=\"tail-link-area\">\n      <foreignObject x=\"300\" y=\"737\" width=\"480\" height=\"160\">\n        <a\n          xmlns=\"http://www.w3.org/1999/xhtml\"\n          linktype=\"image\"\n          href=\"https://macosabc.com/token\">\n          <svg\n            style=\"display:block;pointer-events:visible;\"\n            viewBox=\"0 0 480 160\"\n            width=\"100%\"\n            xmlns=\"http://www.w3.org/2000/svg\"\n            role=\"img\"\n            aria-label=\"插图\"></svg>\n        </a>\n      </foreignObject>\n    </g>\n  </svg>\n</section>`;\nfor (let i = 0; i < allItems.length; i++) {\n  let content = allItems[i].json.content || '';\n  const lastCloseDiv = content.lastIndexOf('</div>');\n  if (lastCloseDiv !== -1) {\n    content = content.slice(0, lastCloseDiv) + '\\n' + tailSvg + '\\n' + content.slice(lastCloseDiv);\n  } else {\n    content += '\\n' + tailSvg;\n  }\n  results.push({ json: { ...allItems[i].json, content }, binary: allItems[i].binary, pairedItem: { item: i } });\n}\nreturn results;"
      },
      "id": "d3fef814-d468-4cfa-9596-0026972cfc03",
      "name": "尾图",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        816,
        224
      ]
    },
    {
      "parameters": {},
      "id": "b57f11b2-996e-4254-a4dc-fb374c8038dd",
      "name": "合并数据",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        1088,
        336
      ]
    },
    {
      "parameters": {
        "fileSelector": "/home/node/.n8n-files/lmg/*.jpg",
        "options": {}
      },
      "id": "30c8d97d-296d-4e15-afe0-a3d8a4c5a145",
      "name": "读取封面图",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1.1,
      "position": [
        448,
        480
      ]
    },
    {
      "parameters": {
        "jsCode": "const inputItems = $input.all();\n\nfunction getItemsFromNode(nodeName) {\n  try {\n    return $(nodeName).all();\n  } catch (error) {\n    return [];\n  }\n}\n\nlet articles = getItemsFromNode('尾图').filter(item => item.json.itemType === 'article');\nif (articles.length === 0) {\n  articles = getItemsFromNode('文章格式处理').filter(item => item.json.itemType === 'article');\n}\nif (articles.length === 0) {\n  articles = inputItems.filter(item => item.json.itemType === 'article' || item.json.content);\n}\n\nlet covers = getItemsFromNode('提取封面ID').filter(item => item.json.itemType === 'cover');\nif (covers.length === 0) {\n  covers = inputItems.filter(item => item.json.itemType === 'cover' || item.json.thumb_media_id);\n}\n\narticles.sort((a, b) => String(a.json.fileName || '').localeCompare(String(b.json.fileName || '')));\ncovers.sort((a, b) => String(a.json.cover_baseName || a.json.fileName || '').localeCompare(String(b.json.cover_baseName || b.json.fileName || '')));\n\nif (articles.length === 0) {\n  throw new Error('No article items found. Check 文章格式处理/尾图 output before creating WeChat drafts.');\n}\nif (covers.length === 0) {\n  throw new Error('No cover items found. Check 上传封面到微信/提取封面ID output before creating WeChat drafts.');\n}\n\nconst coverMap = {};\nfor (const cover of covers) {\n  if (cover.json.cover_baseName && cover.json.thumb_media_id) {\n    coverMap[cover.json.cover_baseName] = cover.json.thumb_media_id;\n  }\n}\n\nconst failures = [];\n\nconst tokenResp = await this.helpers.httpRequest({\n  method: 'GET',\n  url: 'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=wx0abf66f7e4ac36bb&secret=ac4192df2c47fcd3fbd04b9e2662878f',\n});\nconst tokenData = typeof tokenResp === 'string' ? JSON.parse(tokenResp) : tokenResp;\nif (!tokenData.access_token) {\n  throw new Error('access_token failed: ' + JSON.stringify(tokenData));\n}\n\nconst draftArticles = [];\nconst articleMeta = [];\n\nfor (let i = 0; i < articles.length; i++) {\n  const content = articles[i].json.content || '';\n  const fileName = articles[i].json.fileName || 'article';\n  const title = articles[i].json.title || fileName.replace(/\\.md$/, '');\n  const baseName = fileName.replace(/\\.md$/, '');\n  const thumbMediaId = coverMap[baseName];\n\n  if (!thumbMediaId) {\n    const failure = { title, file_name: fileName, error: 'No cover for ' + baseName };\n    failures.push(failure);\n    continue;\n  }\n\n  const digest = content.replace(/<[^>]+>/g, '').slice(0, 120).trim();\n  draftArticles.push({\n    title,\n    author: 'HF Daily',\n    digest,\n    show_cover_pic: 1,\n    content,\n    content_source_url: 'https://macosabc.com/token',\n    thumb_media_id: thumbMediaId,\n  });\n\n  articleMeta.push({\n      title,\n      file_name: fileName,\n      thumb_media_id: thumbMediaId,\n  });\n}\n\nif (failures.length > 0) {\n  throw new Error('Draft creation failed: ' + JSON.stringify(failures));\n}\nif (draftArticles.length === 0) {\n  throw new Error('No drafts were created. article_count=' + articles.length + ', cover_count=' + covers.length);\n}\n\nconst draftResp = await this.helpers.httpRequest({\n  method: 'POST',\n  url: 'https://api.weixin.qq.com/cgi-bin/draft/add?access_token=' + tokenData.access_token,\n  body: { articles: draftArticles },\n  json: true,\n});\nconst draftData = typeof draftResp === 'string' ? JSON.parse(draftResp) : draftResp;\n\nif (draftData.errcode && draftData.errcode !== 0) {\n  throw new Error('Draft creation failed: ' + JSON.stringify({\n    error: draftData,\n    article_count: draftArticles.length,\n    articles: articleMeta,\n  }));\n}\n\nreturn [{\n  json: {\n    success: true,\n    media_id: draftData.media_id,\n    article_count: draftArticles.length,\n    cover_count: covers.length,\n    titles: articleMeta.map(item => item.title),\n    files: articleMeta.map(item => item.file_name),\n    thumb_media_ids: articleMeta.map(item => item.thumb_media_id),\n  },\n}];\n"
      },
      "id": "2d798251-2d85-4143-a4fd-711437d197c6",
      "name": "推送微信公众号草稿",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1440,
        352
      ]
    },
    {
      "parameters": {
        "resource": "media",
        "operation": "media:uploadOther"
      },
      "id": "wc-upload-005",
      "name": "上传封面到微信",
      "type": "n8n-nodes-wechat-offiaccount.wechatOfficialAccountNode",
      "typeVersion": 1,
      "position": [
        672,
        480
      ],
      "credentials": {
        "wechatOfficialAccountCredentialsApi": {
          "id": "VbzKKnTIUC0ZMmRp",
          "name": "Wechat Official Account Credentials account"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "const coverItems = $('读取封面图').all();\nconst uploadItems = $input.all();\nconst results = [];\nfor (let i = 0; i < uploadItems.length; i++) {\n  const mediaId = uploadItems[i].json.media_id || '';\n  const fileName = coverItems[i].json.fileName || '';\n  const baseName = fileName.replace(/\\.jpg$/, '').replace(/\\.png$/, '');\n  results.push({ json: { itemType: 'cover', cover_baseName: baseName, thumb_media_id: mediaId } });\n}\nreturn results;"
      },
      "id": "extract-005",
      "name": "提取封面ID",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        896,
        480
      ]
    },
    {
      "parameters": {
        "jsCode": "\nconst allItems = $input.all();\nconst results = [];\nconst failed = [];\n\nfor (let i = 0; i < allItems.length; i++) {\n  try {\n    const buf = await this.helpers.getBinaryDataBuffer(i, 'data');\n    let content = buf.toString('utf-8');\n    const fileName = allItems[i].json.fileName || 'article';\n\n    let title = fileName.replace(/\\.md$/, '');\n    const firstLines = content.split('\\n');\n    for (const line of firstLines) {\n      const trimmed = line.trim();\n      if (trimmed.length > 0) {\n        title = trimmed.replace(/^#+\\s*/, '').replace(/\\*\\*/g, '').slice(0, 64);\n        break;\n      }\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) + '</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      },\n      binary: allItems[i].binary,\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": "9dd80653-7631-4196-821e-583dc9ffeb48",
      "name": "文章格式处理",
      "type": "n8n-nodes-base.code",
      "typeVersion": 1,
      "position": [
        624,
        224
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 10,
              "triggerAtMinute": 20
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        128,
        448
      ],
      "id": "c28b1d61-6638-40ca-9c58-225894849cc2",
      "name": "Schedule Trigger"
    }
  ],
  "connections": {
    "手动触发": {
      "main": [
        [
          {
            "node": "读取文章",
            "type": "main",
            "index": 0
          },
          {
            "node": "读取封面图",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "读取文章": {
      "main": [
        [
          {
            "node": "文章格式处理",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "尾图": {
      "main": [
        [
          {
            "node": "合并数据",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "合并数据": {
      "main": [
        [
          {
            "node": "推送微信公众号草稿",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "读取封面图": {
      "main": [
        [
          {
            "node": "上传封面到微信",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "上传封面到微信": {
      "main": [
        [
          {
            "node": "提取封面ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "提取封面ID": {
      "main": [
        [
          {
            "node": "合并数据",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "文章格式处理": {
      "main": [
        [
          {
            "node": "尾图",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "读取文章",
            "type": "main",
            "index": 0
          },
          {
            "node": "读取封面图",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {
    "手动触发": [
      {
        "isArtificialRecoveredEventItem": true
      }
    ]
  },
  "meta": {
    "aiBuilderAssisted": true,
    "builderVariant": "mcp",
    "instanceId": "f42815087320c088ecb6cbaef3ecc986b9f5c4b8f6e81b3273a6fc429b262686"
  }
}

第四步:维护删除垃圾

{
  "nodes": [
    {
      "parameters": {},
      "id": "a9e0e97c-9a33-4151-920a-ec35f1ae1505",
      "name": "手动触发",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "command": "find /home/node/.n8n-files/articles /home/node/.n8n-files/lmg -type f -not -name .DS_Store -delete"
      },
      "id": "b983e339-65c2-4b95-bd1c-2262e2ad1a91",
      "name": "清空文件",
      "type": "n8n-nodes-base.executeCommand",
      "typeVersion": 1,
      "position": [
        224,
        0
      ]
    }
  ],
  "connections": {
    "手动触发": {
      "main": [
        [
          {
            "node": "清空文件",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {
    "手动触发": [
      {}
    ]
  },
  "meta": {
    "aiBuilderAssisted": true,
    "builderVariant": "mcp",
    "instanceId": "f42815087320c088ecb6cbaef3ecc986b9f5c4b8f6e81b3273a6fc429b262686"
  }
}

THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容