
Horizon:
n8n:
本项目用到的AItoken免费调用使用推荐站:
查看成品公众号文章:
n8n工作流代码:可直接复制导入
以下为工作流核心参考代码,如需交流沟通定制开发 可联系站长
第一步:生产文章
{
"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















暂无评论内容