Telegram论坛脉搏:使用Gemini和Groq AI模型的社区监控
高级
这是一个Miscellaneous、AI Chatbot、Multimodal AI领域的自动化工作流,包含 59 个节点。主要使用 If、Set、Code、Html、Merge 等节点。 Telegram论坛脉搏:使用Gemini和Groq AI模型的社区监控
前置要求
- •Telegram Bot Token
- •可能需要目标 API 的认证凭证
- •MongoDB 连接字符串
- •Google Gemini API Key
使用的节点 (59 个)
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
"meta": {
"instanceId": "735886904af210643f438394a538e64374f0cb4ab13fd94d97005987482d652a",
"templateCredsSetupCompleted": true
},
"nodes": [
{
"id": "6374695d-e955-4564-9f72-954edfa93e89",
"name": "Groq 聊天模型",
"type": "@n8n/n8n-nodes-langchain.lmChatGroq",
"position": [
-4640,
32
],
"parameters": {
"model": "openai/gpt-oss-120b",
"options": {}
},
"credentials": {
"groqApi": {
"id": "n97HjLNwPywpbFTr",
"name": "Groq jayracroi@gmail.com"
}
},
"typeVersion": 1
},
{
"id": "dfe59ad7-1801-4d7b-b95b-656a84c06d5a",
"name": "多表:您可以连接多个表以实现有组织的数据结构",
"type": "@n8n/n8n-nodes-langchain.outputParserStructured",
"position": [
-4256,
32
],
"parameters": {
"autoFix": true,
"jsonSchemaExample": "{\n \"intent\": \"search\",\n \"keyword\": \"Webhook signature verification\",\n \"platforms\": \"all\",\n \"reddit_sort\": \"top\",\n \"reddit_time\": \"year\",\n \"n8ncom_sort\": \"likes\",\n \"n8ncom_time\": \"after:2025-01-01\",\n \"limit\": 10,\n \"page\": 1,\n \"link_url\": \"https://nguyenthieutoan.com\",\n \"language\": \"vi\",\n \"confidence\": 0.95\n}"
},
"typeVersion": 1.3
},
{
"id": "f5b191a7-908d-48e8-ba20-dcc758c014e9",
"name": "MongoDB 聊天记忆",
"type": "@n8n/n8n-nodes-langchain.memoryMongoDbChat",
"position": [
-4448,
16
],
"parameters": {
"sessionKey": "={{ $('Telegram Trigger - User Message').item.json.message.from.id }}",
"sessionIdType": "customKey",
"collectionName": "n8n_forum_update_chat"
},
"credentials": {
"mongoDb": {
"id": "HNBPi26RUqur8N9y",
"name": "MongoDB_toannguyen96vn_n8n_data"
}
},
"typeVersion": 1
},
{
"id": "2f34f6d8-9121-42b5-945e-800b715e8eaa",
"name": "如果是 Reddit?",
"type": "n8n-nodes-base.httpRequest",
"position": [
-2912,
304
],
"parameters": {
"url": "={{ $json.output.link_url }}",
"options": {}
},
"typeVersion": 4.2
},
{
"id": "2378b2e7-79bf-4bce-b83b-7da7c3fd4ea7",
"name": "评论",
"type": "n8n-nodes-base.httpRequest",
"position": [
-2464,
160
],
"parameters": {
"url": "=https://www.reddit.com{{ $json.comment_url }}",
"options": {}
},
"typeVersion": 4.2
},
{
"id": "8587a97b-abcd-443d-bf32-78369a0d1ade",
"name": "获取帖子内容",
"type": "n8n-nodes-base.html",
"position": [
-2688,
304
],
"parameters": {
"options": {},
"operation": "extractHtmlContent",
"extractionValues": {
"values": [
{
"key": "post_title",
"cssSelector": "h1"
},
{
"key": "post_author",
"cssSelector": "a.author-name"
},
{
"key": "post_content",
"cssSelector": "div[slot='text-body']"
},
{
"key": "post_upvotes",
"attribute": "score",
"cssSelector": "shreddit-post",
"returnValue": "attribute"
},
{
"key": "commentCount",
"attribute": "comment-count",
"cssSelector": "shreddit-post",
"returnValue": "attribute"
},
{
"key": "postTime",
"attribute": "ts",
"cssSelector": "faceplate-timeago",
"returnValue": "attribute"
},
{
"key": "flair",
"cssSelector": "shreddit-post-flair .flair-content"
},
{
"key": "comment_url",
"attribute": "src",
"cssSelector": "faceplate-partial[src^=\"/svc/shreddit/comments/\"]",
"returnValue": "attribute"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "e0e5646a-cb32-457a-9cb3-24b76939f31e",
"name": "获取评论",
"type": "n8n-nodes-base.html",
"position": [
-2240,
160
],
"parameters": {
"options": {
"trimValues": true,
"cleanUpText": true
},
"operation": "extractHtmlContent",
"extractionValues": {
"values": [
{
"key": "comment_author",
"attribute": "author",
"cssSelector": "shreddit-comment",
"returnArray": true,
"returnValue": "attribute"
},
{
"key": "comment_content",
"cssSelector": "shreddit-comment div[slot='comment']",
"returnArray": true,
"returnValue": "html"
},
{
"key": "comment_upvotes",
"attribute": "score",
"cssSelector": "shreddit-comment",
"returnArray": true,
"returnValue": "attribute"
},
{
"key": "comment_level",
"attribute": "depth",
"cssSelector": "shreddit-comment",
"returnArray": true,
"returnValue": "attribute"
},
{
"key": "comment_id",
"attribute": "thingid",
"cssSelector": "shreddit-comment",
"returnArray": true,
"returnValue": "attribute"
},
{
"key": "parent_id",
"attribute": "parentid",
"cssSelector": "shreddit-comment",
"returnArray": true,
"returnValue": "attribute"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "8d7a273d-12a7-4795-9a2e-c3350c13c655",
"name": "如果是 n8n 社区?",
"type": "n8n-nodes-base.httpRequest",
"position": [
-2912,
496
],
"parameters": {
"url": "={{ $('Detect User Intent').item.json.output.link_url }}",
"options": {}
},
"typeVersion": 4.2
},
{
"id": "fc07b8f6-0dd8-4406-9301-bfc29a1f511f",
"name": "获取主题内容",
"type": "n8n-nodes-base.html",
"position": [
-2464,
400
],
"parameters": {
"options": {},
"operation": "extractHtmlContent",
"extractionValues": {
"values": [
{
"key": "topic_title",
"cssSelector": "#topic-title a"
},
{
"key": "category",
"cssSelector": ".topic-category .category-name"
},
{
"key": "original_poster",
"cssSelector": "#post_1 .creator span[itemprop='name']"
},
{
"key": "original_post_content",
"cssSelector": "#post_1 div.post[itemprop='text']\t"
},
{
"key": "original_post_likes",
"cssSelector": "#post_1 .post-likes"
},
{
"key": "original_post_time",
"attribute": "datetime",
"cssSelector": "#post_1 time.post-time",
"returnValue": "attribute"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "b15abbe6-1436-4967-b72e-06d12524a289",
"name": "获取评论1",
"type": "n8n-nodes-base.html",
"position": [
-2688,
592
],
"parameters": {
"options": {},
"operation": "extractHtmlContent",
"extractionValues": {
"values": [
{
"key": "comment_author",
"cssSelector": "div.crawler-post[itemprop='comment'] span[itemprop='name']",
"returnArray": true
},
{
"key": "comment_content",
"cssSelector": "div.crawler-post[itemprop='comment'] div.post[itemprop='text']",
"returnArray": true,
"returnValue": "html"
},
{
"key": "comment_likes",
"cssSelector": "div.crawler-post[itemprop='comment'] .post-likes",
"returnArray": true
},
{
"key": "comment_post_number",
"cssSelector": "div.crawler-post[itemprop='comment'] span[itemprop='position']",
"returnArray": true
},
{
"key": "comment_time",
"attribute": "datetime",
"cssSelector": "div.crawler-post[itemprop='comment'] time.post-time",
"returnArray": true,
"returnValue": "attribute"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "3ca253c1-d29d-4675-93a1-736fc0a9cd87",
"name": "平台?",
"type": "n8n-nodes-base.switch",
"position": [
-3056,
-928
],
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "39188959-a286-43a4-b9b7-c8cd2667e225",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.output.platforms }}",
"rightValue": "=all"
}
]
}
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "9cc13c1f-e903-4c51-89de-ebd290888cef",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.output.platforms }}",
"rightValue": "reddit"
}
]
}
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "940f40a0-4cb0-41a7-8c5e-7e553bc1acf0",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.output.platforms }}",
"rightValue": "n8ncom"
}
]
}
}
]
},
"options": {}
},
"typeVersion": 3.2
},
{
"id": "298bcc69-e598-47c8-97e3-9b874cf03f2d",
"name": "MongoDB 聊天记忆1",
"type": "@n8n/n8n-nodes-langchain.memoryMongoDbChat",
"position": [
-1136,
-32
],
"parameters": {
"sessionKey": "={{ $('Telegram Trigger - User Message').item.json.message.from.id }}",
"sessionIdType": "customKey",
"collectionName": "n8n_forum_update_chat"
},
"credentials": {
"mongoDb": {
"id": "HNBPi26RUqur8N9y",
"name": "MongoDB_toannguyen96vn_n8n_data"
}
},
"typeVersion": 1
},
{
"id": "7a47b59e-0d9a-433d-85e5-13948bcd4351",
"name": "Google Gemini 聊天模型",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
-1264,
-32
],
"parameters": {
"options": {}
},
"credentials": {
"googlePalmApi": {
"id": "QHq86L5qdaq8kmJh",
"name": "Gemini jaynguyena01@gmail.com"
}
},
"typeVersion": 1
},
{
"id": "663ee3fe-b818-4194-9868-1b37d6a8d9ae",
"name": "合并来源1",
"type": "n8n-nodes-base.merge",
"position": [
-4224,
-1168
],
"parameters": {
"mode": "combine",
"options": {},
"combinationMode": "mergeByPosition"
},
"typeVersion": 2
},
{
"id": "58fa20c0-632b-41ac-9596-3daa9c553529",
"name": "仅获取搜索结果1",
"type": "n8n-nodes-base.code",
"position": [
-4448,
-1184
],
"parameters": {
"jsCode": "// Lấy dữ liệu Reddit Fetch từ item đầu vào\nconst redditData = items[0].json.data.children;\n\n// Tạo array mới gồm các thông tin chi tiết hơn cho mỗi kết quả\nconst detailedResults = redditData.map(item => {\n const data = item.data;\n return {\n id: data.id,\n subreddit: data.subreddit,\n title: data.title,\n selftext: data.selftext,\n author: data.author,\n created_utc: data.created_utc,\n created: new Date(data.created_utc * 1000).toLocaleString('vi-VN'),\n url: 'https://www.reddit.com' + data.permalink,\n num_comments: data.num_comments,\n score: data.score,\n upvote_ratio: data.upvote_ratio,\n is_original_content: data.is_original_content,\n flair: data.link_flair_text,\n thumbnail: data.thumbnail || null\n // Có thể bổ sung thêm fields nếu cần\n }\n});\n\n// Gói toàn bộ vào 1 trường \"reddit search result\"\nreturn [\n {\n json: {\n \"reddit search result\": detailedResults\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "6e3f7c4e-a85e-4ce5-8896-40df7b17eeb3",
"name": "MongoDB 聊天记忆2",
"type": "@n8n/n8n-nodes-langchain.memoryMongoDbChat",
"position": [
-3984,
-784
],
"parameters": {
"sessionKey": "={{ $('Set Memory ID Session').item.json.MyTelegramID }}",
"sessionIdType": "customKey",
"collectionName": "n8n_forum_update_chat"
},
"credentials": {
"mongoDb": {
"id": "HNBPi26RUqur8N9y",
"name": "MongoDB_toannguyen96vn_n8n_data"
}
},
"typeVersion": 1
},
{
"id": "a6c0739d-9e4c-4b51-a80c-9262679e43af",
"name": "Google Gemini聊天模型1",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
-4080,
-784
],
"parameters": {
"options": {}
},
"credentials": {
"googlePalmApi": {
"id": "QHq86L5qdaq8kmJh",
"name": "Gemini jaynguyena01@gmail.com"
}
},
"typeVersion": 1
},
{
"id": "1dfa4b4b-5520-4e51-ab4c-996d02e28c16",
"name": "包含关键词?",
"type": "n8n-nodes-base.if",
"position": [
-2832,
-1008
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "5f8cfce1-1b96-47f5-bf92-0af396ff33e1",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.output.keyword }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "94a4c032-240c-4f5a-9f85-931a24ae6cf7",
"name": "包含时间?",
"type": "n8n-nodes-base.if",
"position": [
-2608,
-912
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "or",
"conditions": [
{
"id": "34b96dd6-1da2-44e5-824e-82ed4e13a894",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.output.reddit_sort }}",
"rightValue": "top"
},
{
"id": "274e03b5-5692-4132-8fbe-26520efe7965",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.output.reddit_sort }}",
"rightValue": "relevance"
},
{
"id": "0d668583-7163-41fc-81df-aebbd5aced96",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.output.reddit_sort }}",
"rightValue": "comments"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "441f9d16-c686-4bfb-8b7a-8db094e8effc",
"name": "Reddit 搜索页面 JSON(含时间)",
"type": "n8n-nodes-base.httpRequest",
"position": [
-2384,
-816
],
"parameters": {
"url": "=https://www.reddit.com/r/n8n/search.json?q={{ $json.output.keyword }}&restrict_sr=1&sort={{ $('Detect User Intent').item.json.output.reddit_sort }}&t={{ $('Detect User Intent').item.json.output.reddit_time }}&limit={{ $json.output.limit }}",
"options": {}
},
"typeVersion": 4
},
{
"id": "30dc7838-f81b-46bb-9f7f-178b67ce0b76",
"name": "Reddit 搜索页面(无时间)",
"type": "n8n-nodes-base.httpRequest",
"position": [
-2384,
-1008
],
"parameters": {
"url": "=https://www.reddit.com/r/n8n/search.json?q={{ $json.output.keyword }}+&restrict_sr=1&sort={{ $json.output.reddit_sort }}&limit={{ $json.output.limit }}",
"options": {}
},
"typeVersion": 4
},
{
"id": "7ec168e5-9121-453e-84bb-5b8404cb97ad",
"name": "Reddit 搜索页面 JSON(无关键词)",
"type": "n8n-nodes-base.httpRequest",
"position": [
-2384,
-1200
],
"parameters": {
"url": "=https://www.reddit.com/r/n8n/{{ $json.output.reddit_sort }}.json?t={{ $json.output.reddit_time }}&limit={{ $json.output.limit }}",
"options": {}
},
"typeVersion": 4
},
{
"id": "1164897e-b75f-4424-b5c3-9bda2c37116c",
"name": "仅获取 Reddit 搜索结果",
"type": "n8n-nodes-base.code",
"position": [
-2160,
-1008
],
"parameters": {
"jsCode": "const redditData = items[0].json.data.children;\nconst detailedResults = redditData.map(item => {\n const data = item.data;\n return {\n id: data.id,\n subreddit: data.subreddit,\n title: data.title,\n selftext: data.selftext,\n author: data.author,\n created_utc: data.created_utc,\n created: new Date(data.created_utc * 1000).toLocaleString('vi-VN'),\n url: 'https://www.reddit.com' + data.permalink,\n num_comments: data.num_comments,\n score: data.score,\n upvote_ratio: data.upvote_ratio,\n is_original_content: data.is_original_content,\n flair: data.link_flair_text,\n thumbnail: data.thumbnail || null\n }\n});\nreturn [\n {\n json: {\n \"reddit search result\": detailedResults\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "02bc28d4-3228-464b-adb7-8ce0a99c6d40",
"name": "n8n 社区获取 JSON",
"type": "n8n-nodes-base.httpRequest",
"position": [
-2384,
-624
],
"parameters": {
"url": "=https://community.n8n.io/search.json?q={{ $json.output.keyword }}%20{{ $json.output.n8ncom_time }}%20min_posts%3A0%20in%3Afirst%20{{ $json.output.limit }}%20order%3A{{ $json.output.n8ncom_sort }}",
"options": {}
},
"typeVersion": 4
},
{
"id": "57f5c5c6-abfa-49e6-af85-3d3d14dd43dc",
"name": "仅获取 n8n 社区搜索结果",
"type": "n8n-nodes-base.code",
"position": [
-2160,
-624
],
"parameters": {
"jsCode": "return [\n {\n json: {\n \"n8n community search result\": items.map(item => item.json)\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "f9a040e9-c186-40f9-841c-6c82431e6fd4",
"name": "打开 Reddit 或 n8n 社区链接?",
"type": "n8n-nodes-base.switch",
"position": [
-3136,
480
],
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "81df18c7-c944-4dbd-949c-7b40f9e072dd",
"operator": {
"type": "string",
"operation": "contains"
},
"leftValue": "={{ $json.output.link_url }}",
"rightValue": "reddit.com"
}
]
}
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "5c9f113c-79af-4830-8029-bbbc2fb10280",
"operator": {
"type": "string",
"operation": "contains"
},
"leftValue": "={{ $json.output.link_url }}",
"rightValue": "community.n8n.io"
}
]
}
}
]
},
"options": {}
},
"typeVersion": 3.2
},
{
"id": "d2196535-97ca-414c-b85b-dcd35a9519ab",
"name": "评论摘要 1",
"type": "n8n-nodes-base.code",
"position": [
-2016,
160
],
"parameters": {
"jsCode": "function stripHtml(html) {\n return html\n .replace(/<[^>]+>/g, \"\") // loại tag html\n .replace(/\\s+/g, \" \") // nhiều khoảng trắng thành 1\n .trim();\n}\n\nconst arr = [];\nconst data = items[0].json;\n\nfor (let i = 0; i < data.comment_id.length; i++) {\n arr.push({\n comment_id: data.comment_id[i],\n parent_id: data.parent_id[i] ? data.parent_id[i] : null,\n level: Number(data.comment_level[i]),\n author: data.comment_author[i],\n upvotes: Number(data.comment_upvotes[i]),\n content: stripHtml(data.comment_content[i])\n });\n}\n\n// Kết quả trả về là 1 object có key là \"comment\", value là array các comment\nreturn [{ json: { comment: arr } }];\n"
},
"typeVersion": 2
},
{
"id": "e059fea5-05a0-4310-9403-3aba0997d5f7",
"name": "评论摘要 2",
"type": "n8n-nodes-base.code",
"position": [
-2464,
592
],
"parameters": {
"jsCode": "// Hàm loại bỏ tag html (đơn giản, có thể mở rộng nếu cần)\nfunction stripHtml(html) {\n return html\n .replace(/<[^>]+>/g, \"\")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nconst data = items[0].json; // Data đầu vào bạn gửi\n\nconst result = [];\nconst len = data.comment_author.length; // Sẽ đồng bộ với các mảng còn lại\n\nfor (let i = 0; i < len; i++) {\n result.push({\n author: data.comment_author[i],\n content: stripHtml(data.comment_content[i]),\n likes: data.comment_likes[i],\n post_number: data.comment_post_number[i],\n time: data.comment_time[i]\n // Thêm các trường khác nếu muốn như comment_id, parent_id,... nếu có!\n });\n}\n\nreturn [{ json: { comment: result } }];\n"
},
"typeVersion": 2
},
{
"id": "330ec503-f633-4b7a-b6f2-16f5d5501f10",
"name": "合并链接内容",
"type": "n8n-nodes-base.merge",
"position": [
-1792,
432
],
"parameters": {
"mode": "combine",
"options": {},
"combineBy": "combineByPosition"
},
"typeVersion": 3.2
},
{
"id": "3f33bbb0-d19d-4471-8094-3b8616d421f5",
"name": "合并搜索结果",
"type": "n8n-nodes-base.merge",
"position": [
-1936,
-816
],
"parameters": {
"mode": "combine",
"options": {},
"combinationMode": "mergeByPosition"
},
"typeVersion": 2
},
{
"id": "6e91f239-5580-493f-b5fd-842e5ffa414b",
"name": "包装为数据对象",
"type": "n8n-nodes-base.code",
"position": [
-1408,
-256
],
"parameters": {
"jsCode": "return [\n {\n json: {\n data: items.map(item => item.json)\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "f665afa8-3a9e-48fd-9276-85193319221c",
"name": "发送回复",
"type": "n8n-nodes-base.telegram",
"position": [
-768,
-256
],
"webhookId": "cadf2d35-187a-4823-8f17-d08ed3489f2c",
"parameters": {
"text": "={{ $json.text }}",
"chatId": "={{ $('Telegram Trigger - User Message').item.json.message.from.id }}",
"additionalFields": {
"parse_mode": "HTML",
"appendAttribution": false
}
},
"credentials": {
"telegramApi": {
"id": "o4Df0vGzQkRsas07",
"name": "n8n Notification Bot"
}
},
"typeVersion": 1.2
},
{
"id": "43930eb5-673d-4d5f-a87b-c80932cf1c53",
"name": "计划触发器",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-4896,
-1088
],
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 8
}
]
}
},
"typeVersion": 1
},
{
"id": "1b85f710-e828-4b76-89f0-f07efff16922",
"name": "Reddit",
"type": "n8n-nodes-base.httpRequest",
"position": [
-4672,
-1184
],
"parameters": {
"url": "=https://www.reddit.com/r/n8n/top.json?t=day&limit=20",
"options": {}
},
"typeVersion": 4
},
{
"id": "f0e544ac-71bc-4341-a99f-06d704e4f37e",
"name": "n8n 社区获取",
"type": "n8n-nodes-base.httpRequest",
"position": [
-4672,
-992
],
"parameters": {
"url": "=https://community.n8n.io/search.json?q=%20after:{{ (new Date(Date.now() - 1 * 24 * 60 * 60 * 1000)).toISOString().slice(0,10) }}%20min_posts%3A0%20in%3Afirst%2010%20order%3Alatest",
"options": {}
},
"typeVersion": 4
},
{
"id": "787c3a75-5adf-4224-82e6-352e8bbb21e3",
"name": "仅获取搜索结果2",
"type": "n8n-nodes-base.code",
"position": [
-4448,
-992
],
"parameters": {
"jsCode": "return [\n {\n json: {\n \"n8n community search result\": items.map(item => item.json)\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "989d3b40-cd71-486a-b2bf-a8aed22d801b",
"name": "发送自动回复",
"type": "n8n-nodes-base.telegram",
"position": [
-3488,
-944
],
"webhookId": "cadf2d35-187a-4823-8f17-d08ed3489f2c",
"parameters": {
"text": "={{ $json.text }}",
"chatId": "=6163095869",
"additionalFields": {
"parse_mode": "HTML",
"appendAttribution": false
}
},
"credentials": {
"telegramApi": {
"id": "o4Df0vGzQkRsas07",
"name": "n8n Notification Bot"
}
},
"typeVersion": 1.2
},
{
"id": "41f9a6c5-fb93-469a-a3c5-c14a518f97a5",
"name": "MongoDB 聊天记忆3",
"type": "@n8n/n8n-nodes-langchain.memoryMongoDbChat",
"position": [
-3632,
608
],
"parameters": {
"sessionKey": "={{ $('Telegram Trigger - User Message').item.json.message.from.id }}",
"sessionIdType": "customKey",
"collectionName": "n8n_forum_update_chat"
},
"credentials": {
"mongoDb": {
"id": "HNBPi26RUqur8N9y",
"name": "MongoDB_toannguyen96vn_n8n_data"
}
},
"typeVersion": 1
},
{
"id": "89f73382-8fad-465c-9a22-0649835ca33e",
"name": "Google Gemini 聊天模型2",
"type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
"position": [
-3808,
608
],
"parameters": {
"options": {}
},
"credentials": {
"googlePalmApi": {
"id": "QHq86L5qdaq8kmJh",
"name": "Gemini jaynguyena01@gmail.com"
}
},
"typeVersion": 1
},
{
"id": "6d52e5f7-b2f5-4bda-b936-887378afdf0f",
"name": "清理和分块2",
"type": "n8n-nodes-base.code",
"position": [
-4592,
384
],
"parameters": {
"jsCode": "/**\n * Telegram HTML Normalizer + Chunker (≤ 2000 chars)\n * - Markdown → Telegram HTML (b,i,u,s,code,pre,a,br,blockquote)\n * - Map/strip unsupported tags, sanitize attributes\n * - Preserve links/code/pre; do NOT split inside <a>/<pre>/<blockquote> and inline pairs\n * - Close/reopen ONLY long-lived tags per chunk (a, pre, blockquote)\n * - Prevent stray closing tags and treat lone \"<\" safely\n */\n\nconst MAX_LEN = 2000;\n\n/* === Get input === */\nlet text = ($input.first().json.output ?? $input.first().json.text ?? '').trim();\nif (!text) return [];\n\n/* === Allowed Telegram tags === */\nconst allowed = new Set(['a','b','i','u','s','code','pre','br','blockquote']);\n\n/* ---------- Utils ---------- */\nconst escapeHtml = (s) => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n\nfunction sanitizeHref(href){\n if (!href) return '';\n const h = String(href).trim().replace(/^['\"]|['\"]$/g,'');\n const lower = h.toLowerCase();\n if (lower.startsWith('javascript:') || lower.startsWith('data:')) return '';\n return h;\n}\n\n/* ---------- 1) Markdown -> HTML ---------- */\nfunction mdToHtml(md){\n let s = md;\n // code block\n s = s.replace(/```([\\s\\S]*?)```/g, (m,p1)=>`<pre>${escapeHtml(p1.trim())}</pre>`);\n // inline code\n s = s.replace(/`([^`]+)`/g, (m,p1)=>`<code>${escapeHtml(p1)}</code>`);\n // bold / italic / underline / strike\n s = s.replace(/\\*\\*([^*]+)\\*\\*/g, '<b>$1</b>');\n s = s.replace(/(^|[\\s(])\\*([^*\\n]+)\\*(?=$|[\\s).,!?\\]])/g, '$1<i>$2</i>');\n s = s.replace(/__([^_]+)__/g, '<u>$1</u>');\n s = s.replace(/~~([^~]+)~~/g, '<s>$1</s>');\n // headers → bold + newline\n s = s.replace(/^(#{1,6})\\s+(.+)$/gm, (m, hashes, content)=>`<b>${content.trim()}</b>\\n`);\n // list markers → bullet\n s = s.replace(/^[\\-\\*\\+]\\s+(.+)$/gm, '• $1');\n // [text](url)\n s = s.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, (m,txt,url)=>{\n const safe = sanitizeHref(url);\n return safe ? `<a href=\"${safe}\">${txt}</a>` : txt;\n });\n return s;\n}\n\n/* ---------- 2) Protect <pre>/<code> contents ---------- */\nfunction protectPreCodeBlocks(input){\n const placeholders = [];\n let out = input;\n\n out = out.replace(/<pre\\b[^>]*>([\\s\\S]*?)<\\/pre>/gi, (m, inner)=>{\n const token = `__PRE_BLOCK_${placeholders.length}__`;\n placeholders.push({token, tag:'pre', content: escapeHtml(inner)});\n return token;\n });\n\n out = out.replace(/<code\\b[^>]*>([\\s\\S]*?)<\\/code>/gi, (m, inner)=>{\n const token = `__CODE_BLOCK_${placeholders.length}__`;\n placeholders.push({token, tag:'code', content: escapeHtml(inner)});\n return token;\n });\n\n const restore = (s)=>{\n for (const ph of placeholders){\n s = s.split(ph.token).join(`<${ph.tag}>${ph.content}</${ph.tag}>`);\n }\n return s;\n };\n\n return { text: out, restore };\n}\n\n/* ---------- 3) HTML normalize ---------- */\nfunction htmlNormalize(input){\n let s = input;\n\n s = s.replace(/<br\\s*\\/?>/gi, '<br>');\n s = s.replace(/<li[^>]*>/gi, '• ').replace(/<\\/li>/gi, '\\n');\n s = s.replace(/<\\/?(ul|ol)[^>]*>/gi, '');\n\n s = s.replace(/<p[^>]*>/gi, '').replace(/<\\/p>/gi, '\\n\\n');\n s = s.replace(/<div[^>]*>/gi, '').replace(/<\\/div>/gi, '\\n');\n\n s = s.replace(/<h[1-6][^>]*>([\\s\\S]*?)<\\/h[1-6]>/gi, (m, inner)=>`<b>${inner.trim()}</b>\\n`);\n\n // Map aliases -> canonical\n s = s.replace(/<\\/?strong\\b/gi, t=>t.replace(/strong/i,'b'));\n s = s.replace(/<\\/?em\\b/gi, t=>t.replace(/em/i,'i'));\n s = s.replace(/<\\/?ins\\b/gi, t=>t.replace(/ins/i,'u'));\n s = s.replace(/<\\/?(strike|del)\\b/gi, t=>t.replace(/strike|del/i,'s'));\n\n // remove spans\n s = s.replace(/<\\/?span[^>]*>/gi, '');\n\n // images -> textual hint\n s = s.replace(/<img[^>]*src\\s*=\\s*(\"[^\"]+\"|'[^']+'|[^\\s>]+)[^>]*>/gi, (m,src)=>{\n const clean = String(src).replace(/^['\"]|['\"]$/g,'');\n return ` (ảnh: ${clean}) `;\n });\n\n return s;\n}\n\n/* ---------- 3.5) Escape lone '<' that are NOT valid tags ---------- */\nfunction escapeLoneAngles(input){\n // allow only tags in: a|b|i|u|s|code|pre|br|blockquote\n return input.replace(/<(?!\\/?(a|b|i|u|s|code|pre|br|blockquote)\\b)/gi, '<');\n}\n\n/* ---------- 4) Sanitize tags (keep only Telegram tags) ---------- */\nfunction sanitizeHtml(input){\n return input.replace(/<\\/?([a-z0-9]+)(\\s+[^>]*)?>/gi, (full, tag, attrs='')=>{\n const t = tag.toLowerCase();\n\n if (t === 'a'){\n if (full.startsWith('</')) return '</a>';\n const hrefMatch = attrs.match(/href\\s*=\\s*(\"[^\"]+\"|'[^']+'|[^\\s>]+)/i);\n const safeHref = hrefMatch ? sanitizeHref(hrefMatch[1]) : '';\n return safeHref ? `<a href=\"${safeHref}\">` : '';\n }\n\n if (!allowed.has(t)) return '';\n if (t === 'br') return '<br>';\n return full[1] === '/' ? `</${t}>` : `<${t}>`;\n });\n}\n\n/* ---------- 5) Track ONLY long-lived tags across chunks ---------- */\n/* We only track <a>, <pre>, <blockquote> so inline pairs never get auto-closed. */\nfunction getOpenTagsWithAttrs(html){\n const trackable = new Set(['a','pre','blockquote']);\n const stack = [];\n const re = /<\\/?([a-z0-9]+)(?:\\s+[^>]*)?>/gi;\n let m;\n while ((m = re.exec(html))){\n const raw = m[0];\n const t = m[1].toLowerCase();\n if (!trackable.has(t)) continue;\n\n if (raw[1] === '/'){\n const idx = [...stack].reverse().findIndex(e=>e.tag===t);\n if (idx !== -1) stack.splice(stack.length - 1 - idx, 1);\n } else {\n if (t === 'a'){\n const hrefMatch = raw.match(/href\\s*=\\s*(\"[^\"]+\"|'[^']+'|[^\\s>]+)/i);\n const href = hrefMatch ? sanitizeHref(hrefMatch[1]) : '';\n if (href) stack.push({tag:'a', href});\n } else {\n stack.push({tag:t});\n }\n }\n }\n return stack;\n}\nconst closeTags = (open)=>[...open].reverse().map(e=>`</${e.tag}>`).join('');\nconst openTagsStr = (open)=>open.map(e=>e.tag==='a'?`<a href=\"${e.href}\">`:`<${e.tag}>`).join('');\n\n/* ---------- 6) Normalize pipeline ---------- */\nfunction normalizeToTelegramHtml(input){\n const looksLikeMd = /(^|\\s)[*_`~]|^#{1,6}\\s|```/.test(input);\n let s = looksLikeMd ? mdToHtml(input) : input;\n\n s = htmlNormalize(s);\n\n const protector = protectPreCodeBlocks(s);\n s = protector.text;\n\n // avoid \"<10\" etc. being treated as a tag\n s = escapeLoneAngles(s);\n\n s = sanitizeHtml(s);\n s = protector.restore(s);\n\n s = s.replace(/\\n{3,}/g, '\\n\\n').trim();\n return s;\n}\n\n/* ---------- 7) Split safely ---------- */\nfunction splitHtmlSmart(html, maxLen){\n // Atomic segments: a, pre, blockquote, and inline pairs b/i/u/s/code\n const re = /(<a\\b[^>]*>[\\s\\S]*?<\\/a>)|(<pre\\b[^>]*>[\\s\\S]*?<\\/pre>)|(<blockquote\\b[^>]*>[\\s\\S]*?<\\/blockquote>)|(<b\\b[^>]*>[\\s\\S]*?<\\/b>)|(<i\\b[^>]*>[\\s\\S]*?<\\/i>)|(<u\\b[^>]*>[\\s\\S]*?<\\/u>)|(<s\\b[^>]*>[\\s\\S]*?<\\/s>)|(<code\\b[^>]*>[\\s\\S]*?<\\/code>)|(<br\\s*\\/?>)|(<[^>]+>)|([^<]+)/gi;\n\n const segments = [];\n let m;\n while ((m = re.exec(html))){\n segments.push(\n m[1] || m[2] || m[3] || m[4] || m[5] || m[6] || m[7] || m[8] ||\n (m[9] ? '<br>' : (m[10] || m[11]))\n );\n }\n\n const chunks = [];\n let buffer = '';\n let prefixOpen = '';\n\n const pushBuffer = ()=>{\n if (!buffer.trim()) return;\n let out = prefixOpen + buffer;\n // Only close a/pre/blockquote\n const open = getOpenTagsWithAttrs(out);\n out += closeTags(open);\n prefixOpen = openTagsStr(open); // reopen at next chunk if any\n chunks.push(out.trim());\n buffer = '';\n };\n\n // Shrink visible text inside <a> if needed\n const shrinkAnchorIfTooLong = (seg, room)=>{\n const mm = /^<a\\b[^>]*>([\\s\\S]*?)<\\/a>$/i.exec(seg);\n if (!mm) return seg;\n const visible = mm[1];\n if (seg.length <= room) return seg;\n const truncated = (visible.length > room - 30) ? (visible.slice(0, Math.max(3, room - 33)) + '...') : visible;\n return seg.replace(visible, truncated);\n };\n\n // Split huge <pre> / <blockquote> into multiple wrapped chunks\n const splitWrappedBlock = (segTag, seg, available, emit)=>{\n const rx = new RegExp(`^<${segTag}[^>]*>([\\\\s\\\\S]*)<\\\\/${segTag}>$`, 'i');\n const mm = rx.exec(seg);\n if (!mm) return false;\n let content = mm[1];\n const shellLen = (`<${segTag}></${segTag}>`).length; // 11 for pre, 23 for blockquote, etc.\n\n // if it fits already → no split here\n if (seg.length <= available) return false;\n\n // flush current buffer to start fresh\n if (buffer.trim()) pushBuffer();\n\n while (content.length){\n // recompute room each loop (prefixOpen may change)\n const room = Math.max(200, maxLen - prefixOpen.length - shellLen - 10);\n const slice = content.slice(0, room);\n buffer = `<${segTag}>${slice}</${segTag}>`;\n pushBuffer();\n content = content.slice(slice.length);\n }\n return true;\n };\n\n for (const seg of segments){\n const segLen = seg.length;\n\n // if appending seg would overflow\n if ((prefixOpen.length + buffer.length + segLen) > maxLen){\n\n // 7.1 Try shrinking anchor text\n if (/^<a\\b/i.test(seg)){\n const room = maxLen - prefixOpen.length - buffer.length - 1;\n const shrunk = shrinkAnchorIfTooLong(seg, Math.max(60, room));\n if ((prefixOpen.length + buffer.length + shrunk.length) <= maxLen){\n buffer += shrunk;\n continue;\n }\n }\n\n // 7.2 Split huge <pre>/<blockquote> safely (keep wrappers)\n const available = maxLen - prefixOpen.length - buffer.length;\n if (/^<pre\\b/i.test(seg)){\n if (splitWrappedBlock('pre', seg, available)) continue;\n }\n if (/^<blockquote\\b/i.test(seg)){\n if (splitWrappedBlock('blockquote', seg, available)) continue;\n }\n\n // 7.3 Push current chunk\n pushBuffer();\n\n // 7.4 Place seg into new buffer or split plain text / hard-cut tag as last resort\n if ((prefixOpen.length + segLen) <= maxLen){\n buffer = seg;\n } else if (!seg.startsWith('<')) {\n // split plain text by whitespace then hard-cut if needed\n const words = seg.split(/(\\s+)/);\n for (const w of words){\n const candidate = buffer + w;\n if ((prefixOpen.length + candidate.length) > maxLen){\n pushBuffer();\n if ((prefixOpen.length + w.length) > maxLen){\n let s = w;\n const room = Math.max(200, maxLen - prefixOpen.length - 10);\n while (s.length){\n buffer = s.slice(0, room);\n pushBuffer();\n s = s.slice(room);\n }\n } else {\n buffer = w;\n }\n } else {\n buffer = candidate;\n }\n }\n } else {\n // FINAL FALLBACK: extremely long single tag (very rare)\n // Hard-cut by characters (may break styling but keeps length constraints)\n let start = 0;\n const room = Math.max(200, maxLen - prefixOpen.length - 10);\n while (start < segLen){\n buffer = seg.slice(start, start + room);\n pushBuffer();\n start += room;\n }\n }\n continue;\n }\n\n // fits → append\n buffer += seg;\n }\n\n if (buffer.trim()) pushBuffer();\n return chunks;\n}\n\n/* ===== Run ===== */\nconst normalized = normalizeToTelegramHtml(text);\nconst chunks = splitHtmlSmart(normalized, MAX_LEN);\n\n/* Export to Telegram node */\nreturn chunks.map(c => ({ json: { text: c } }));\n"
},
"typeVersion": 2
},
{
"id": "a75a37ff-1b57-4c4d-9d09-82fba7118860",
"name": "发送回复1",
"type": "n8n-nodes-base.telegram",
"position": [
-4224,
384
],
"webhookId": "cadf2d35-187a-4823-8f17-d08ed3489f2c",
"parameters": {
"text": "={{ $json.text }}",
"chatId": "={{ $('Telegram Trigger - User Message').item.json.message.from.id }}",
"additionalFields": {
"parse_mode": "HTML",
"appendAttribution": false,
"reply_to_message_id": "={{ $('Telegram Trigger - User Message').item.json.message.message_id }}"
}
},
"credentials": {
"telegramApi": {
"id": "o4Df0vGzQkRsas07",
"name": "n8n Notification Bot"
}
},
"typeVersion": 1.2
},
{
"id": "d64792e7-d8c4-41ca-b164-bec04cc9b85e",
"name": "便签1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-5872,
-1456
],
"parameters": {
"color": 6,
"width": 832,
"height": 2224,
"content": "# 基于 AI 的 Telegram n8n 论坛助手(使用 Gemini & Groq)"
},
"typeVersion": 1
},
{
"id": "e2f8ac23-84f6-4897-825f-c38b657c9f67",
"name": "便签2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-4960,
-576
],
"parameters": {
"color": 3,
"width": 1584,
"height": 752,
"content": "## 2. 按需用户交互"
},
"typeVersion": 1
},
{
"id": "1772a2c2-3ebe-49f3-9409-da188463c417",
"name": "便签3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-4960,
240
],
"parameters": {
"color": 5,
"width": 1584,
"height": 528,
"content": "## 2.1. 置信度检查与验证(当 `confidence < 0.7` 时必需)"
},
"typeVersion": 1
},
{
"id": "3c2d7129-b9cf-457a-a92c-f094ce733d52",
"name": "便签4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-3312,
-128
],
"parameters": {
"color": 4,
"width": 1792,
"height": 880,
"content": "## 4. 深入探索帖子详情"
},
"typeVersion": 1
},
{
"id": "c5383abd-d858-4fb2-8c12-a7c28ed0436c",
"name": "便签5",
"type": "n8n-nodes-base.stickyNote",
"position": [
-3312,
-1456
],
"parameters": {
"color": 4,
"width": 1792,
"height": 1168,
"content": "## 3. 多平台搜索与合并"
},
"typeVersion": 1
},
{
"id": "335a398a-7b97-4cbe-9c88-f14545032d15",
"name": "便签6",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1456,
-1456
],
"parameters": {
"color": 5,
"width": 864,
"height": 2208,
"content": "## 5. 摘要、格式化和传递"
},
"typeVersion": 1
},
{
"id": "fb446a1e-4386-4856-bf90-3a49a8f0217f",
"name": "Telegram 触发器 - 用户消息",
"type": "n8n-nodes-base.telegramTrigger",
"position": [
-4896,
-240
],
"webhookId": "97ec6f4d-ae85-443d-8604-58f93f0d1695",
"parameters": {
"updates": [
"message"
],
"additionalFields": {}
},
"credentials": {
"telegramApi": {
"id": "o4Df0vGzQkRsas07",
"name": "n8n Notification Bot"
}
},
"typeVersion": 1.2
},
{
"id": "981c5a5e-a19d-4617-b068-8bf57b2ed63b",
"name": "检测用户意图",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
-4528,
-240
],
"parameters": {
"text": "={{ $json.message.text }}",
"options": {
"systemMessage": "=## Role & Context\n\nYou are an AI Agent running inside an n8n workflow created by Nguyễn Thiệu Toàn, and you are also an expert at identifying user intent to decide the workflow’s next action. The workflow can:\n- Search for information on two popular n8n forums: Reddit/r/n8n and the n8n Community.\n- Open a specific link to view and summarize content for the user.\n- Converse and chat with the user.\n\nYou strictly, intelligently, flexibly, and precisely follow the rules set by Nguyễn Thiệu Toàn below:\n\nCurrent time is {{ new Date().toISOString().slice(0,19) }}.\nYou receive the user’s message and conversation history, then return a JSON result following the extended schema so the workflow can route actions correctly.\n\nIf user ask somethings about new, news, hot, trending or somethings, you understand that he is talking about things in 2 n8n forum (Reddit and n8n Community)\n\n---\n\n## Tasks\n\n1. Understand the user’s intent:\n\nIs it to search for n8n information on Reddit r/n8n and the n8n Community?\nOr is it chat (greeting, mood…), open_link (open/view details of a specific post or URL)?\n\n2. Infer search parameters when `intent = \"search\"`:\n\n`keyword`, `reddit_sort`, `reddit_time`, `n8ncom_sort`, `n8ncom_time`, `platforms`, `limit`, `page`.\n---\n\n## Intent classification (priority order)\n\n1. open_link: The user provides or asks to open a specific URL (reddit.com/r/n8n…, community.n8n.io/…, short links) or asks “view details of post #2”, “open the one above”…\n2. search: Contains verbs for searching/lookup/how-to/issue related to n8n and/or explicitly mentions Reddit/n8n Community.\n3. chat: Greetings, emotions, casual talk (“are you happy?”, “hello”, “thanks”…), basic questions that don’t require opening links or searching about n8n.\n\n> If a message can fall into multiple categories, apply the above priority.\n\n---\n\n## Search parameter inference (when `intent = \"search\"`)\n\n### 1) `keyword`\n\nYou must create a highly intelligent keyword based on the user’s request. The keyword must always be in English and as concise as possible to maximize search results, avoiding overly specific words. Extract the core need about n8n. Keep any text inside quotation marks unchanged.\nRemove filler words that don’t add query meaning.\nPrioritize technical phrases: e.g., \"Telegram Trigger 429\", \"Google Sheets append row\", \"HTTP Request OAuth2\", \"AI Agent memory\".\n\n### 2) Sort & Time\n\nReddit → `reddit_sort`: `relevance` (best match – default if missing) · `new` (recent) · `top` (most upvoted/hottest/most liked) · `comments` (most discussed) · `hot` (trending in last 24h).\nn8n Community → `n8ncom_sort`: `views` (most viewed, default if missing) · `latest` (most recent) · `likes` (most liked) · `latest_topic` (most recently updated topics) · `votes` (most voted).\nReddit → `reddit_time`: `hour|day|week|month|year|all`. Default `week`.\nn8n Community → `n8ncom_time`: `after:YYYY-MM-DD` or `before:YYYY-MM-DD`.\n\n Natural language mapping: \"today\" → `after:{{ new Date().toISOString().slice(0, 10) }}`\n “this week” → `after:{{ (new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)).toISOString().slice(0,10) }}`; “past 30 days” → `after:{{ (new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)).toISOString().slice(0,10) }}`; “past 1 year” → `after:{{ (new Date(Date.now() - 365 * 24 * 60 * 60 * 1000)).toISOString().slice(0,10) }}>`; “past X days” → `after:({{ new Date().toISOString().slice(0, 10) }} - X days)`\n\n Prefer returning an absolute date in YYYY-MM-DD based on the current time {{ new Date().toISOString().slice(0, 10) }}. If no data available, default to `after:{{ (new Date(Date.now() - 1 * 24 * 60 * 60 * 1000)).toISOString().slice(0,10) }}` (i.e., since yesterday).\n\n### 3) Platforms & pagination\n\n`platforms`: \"reddit | n8ncom | all\". If the user specifies one place, keep only that. If generic, default to \"all\".\n`limit` defaults to `10` (1–50). `page` defaults to `1`.\n\n---\n\n## Control fields for non-`search` intents\n\nopen_link: fill `link_url` (valid URL). If the user says “post #2” based on the previous list, use the conversation history to find the appropriate link and fill 'link_url'.\n\nchat: If the user intends basic Q&A or casual chat with the bot.\n---\n\n## Normalization & Defaults\n\nAll enum values must be lowercase from the allowed list.\nAlways output all fields in the JSON (no missing fields). For fields without values, output empty string \"\".\nDefaults if missing:\n `reddit_sort = \"relevance\"`, `reddit_time = \"month\"`\n `n8ncom_sort = \"views\"`, `n8ncom_time = \"after:{{ (new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)).toISOString().slice(0,10) }}\"`\n `platforms = [\"reddit\",\"n8ncom\"]`, `limit = 10`, `page = 1`\nDetect the user’s language to set `language` (short ISO, e.g., `vi`, `en`).\nSet `confidence` (0.0–1.0) as the certainty for the `intent` & parameters.\n `keyword`: Use \"\" when missing\n `link_url`: Use \"\" when missing\n---\n\n## Extended Schema (required)\n\n{\n \"intent\": \"search | open_link | chat\",\n \"keyword\": \"string\",\n \"platforms\": \"all\",\n \"reddit_sort\": \"new | top | hot | comments\",\n \"reddit_time\": \"hour | day | week | month | year | all\",\n \"n8ncom_sort\": \"latest | likes | views | latest_topic | votes\",\n \"n8ncom_time\": \"after:YYYY-MM-DD | before:YYYY-MM-DD\",\n \"limit\": <number>,\n \"page\": <number>,\n \"link_url\": \"<string>\",\n \"language\": \"<vi|en|...>\",\n \"confidence\": <number>\n}\n\n> Output must be a valid JSON only per the schema above, without extra description/markdown.\n\n---\n\n## Quick heuristics\n\nSearch keywords: \"search\", \"guide\", \"how to\", \"error\", \"bug\", \"workflow\", \"node\", \"reddit\", \"community\", \"post\", \"topic\".\nSort hints: \"new/recent\" → `new`/`latest`; \"top/best\" → `top`/`likes`; \"most viewed\" → `views`; \"active discussion\" → `comments`/`latest_topic`.\nTime hints: \"today\" → `day`; \"this week\" → `week`; \"this month\" → `month`; \"this year\" → `year`; \"all time\" → `all` / `after:2010-01-01`.\nopen_link: presence of URL or index reference (#1, #2…) → fill `link_url`.\n\nchat: greetings/small talk like \"hello\", \"hi\", \"are you happy\", \"thanks\"…\n\n---\n\n## Input/Output examples\n\n### 1) Search (default both platforms)\n\nInput: \"Find top posts about Webhook signature verification this year\"\n\n{\n \"intent\": \"search\",\n \"keyword\": \"Webhook signature verification\",\n \"platforms\": \"all\",\n \"reddit_sort\": \"top\",\n \"reddit_time\": \"year\",\n \"n8ncom_sort\": \"likes\",\n \"n8ncom_time\": \"after:{{ new Date().getFullYear() }}-01-01\",\n \"limit\": 10,\n \"page\": 1,\n \"link_url\": \"\",\n \"language\": \"vi\",\n \"confidence\": 0.95\n}\n\n### 2) Search (specific platform)\n\nInput: \"Filter new posts about Telegram Trigger 429 on Community this month\"\n\n{\n \"intent\": \"search\",\n \"keyword\": \"Telegram Trigger 429\",\n \"platforms\": \"all\",\n \"reddit_sort\": \"new\",\n \"reddit_time\": \"week\",\n \"n8ncom_sort\": \"latest\",\n \"n8ncom_time\": \"after:{{ new Date().getFullYear() + \"-\" + String(new Date().getMonth() + 1).padStart(2, \"0\") }}-01\",\n \"limit\": 10,\n \"page\": 1,\n \"link_url\": \"\",\n \"language\": \"vi\",\n \"confidence\": 0.9\n}\n\n### 3) Open link (direct URL)\n\nInput: \"Summarize the topic about XXX you mentioned earlier!\"\n\n{\n \"intent\": \"open_link\",\n \"keyword\": \"\",\n \"platforms\": \"all\",\n \"reddit_sort\": \"relevance\",\n \"reddit_time\": \"week\",\n \"n8ncom_sort\": \"views\",\n \"n8ncom_time\": \"after:{{ new Date().getFullYear() + \"-\" + String(new Date().getMonth() + 1).padStart(2, \"0\") }}-01\",\n \"limit\": 10,\n \"page\": 1,\n \"link_url\": \"https://nguyenthieutoan.com\",\n \"language\": \"vi\",\n \"confidence\": 0.98\n}\n\n### 4) Open link (refer by index)\n\nInput: \"Summarize post #2 from the list above\"\n\n{\n \"intent\": \"open_link\",\n \"keyword\": \"\",\n \"platforms\": \"all\",\n \"reddit_sort\": \"relevance\",\n \"reddit_time\": \"week\",\n \"n8ncom_sort\": \"views\",\n \"n8ncom_time\": \"after:2025-08-12\",\n \"limit\": 10,\n \"page\": 1,\n \"link_url\": \"https://nguyenthieutoan.com\",\n \"language\": \"vi\",\n \"confidence\": 0.6\n}\n\n\n### 5) Chat\n\nInput: \"Are you happy?\"\n\n{\n \"intent\": \"chat\",\n \"keyword\": \"\",\n \"platforms\": \"all\",\n \"reddit_sort\": \"relevance\",\n \"reddit_time\": \"week\",\n \"n8ncom_sort\": \"views\",\n \"n8ncom_time\": \"after:{{ new Date().getFullYear() + \"-\" + String(new Date().getMonth() + 1).padStart(2, \"0\") }}-01\",\n \"limit\": 10,\n \"page\": 1,\n \"link_url\": \"\",\n \"language\": \"vi\",\n \"confidence\": 0.9\n}\n"
},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 2.2
},
{
"id": "24d74cc8-a0e1-44b0-8ebb-1bbdc72ac235",
"name": "按意图分支",
"type": "n8n-nodes-base.switch",
"position": [
-3584,
-272
],
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "f4942c18-c543-47c3-a726-fa8f708e5d0f",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.output.intent }}",
"rightValue": "search"
}
]
}
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "3a053610-652c-4a96-8ed1-87ab66195be0",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.output.intent }}",
"rightValue": "chat"
}
]
}
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "9c27b4b8-2bab-4edc-89c0-4113604c18cc",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.output.intent }}",
"rightValue": "open_link"
}
]
}
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "933b4904-e9c5-44e7-91df-d1ad181f2456",
"operator": {
"type": "number",
"operation": "lt"
},
"leftValue": "={{ $json.output.confidence }}",
"rightValue": 0.7
}
]
}
}
]
},
"options": {}
},
"typeVersion": 3.2
},
{
"id": "897009cf-d40d-4e60-b564-4bf64ec88225",
"name": "AI 摘要器澄清",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
-3760,
400
],
"parameters": {
"text": "={{ $('Telegram Trigger - User Message').item.json.message.text }}",
"options": {
"systemMessage": "=Role\nYou are Ken, assistant to Nguyễn Thiệu Toàn. Task: when the current intent’s confidence < 0.7, QUICKLY VERIFY with one short question plus 2–3 clear tap-to-choose options. Use only the given inputs; do not browse the web or infer beyond scope.\n\nInputs\n- user_message: what bro Toàn just asked or typed.\n- intent_info_json: the current inferred intent data (intent, keyword, platform/sort/time, open_link/context_ref, reply, language, confidence…).\n<intent_info_json>\n{{ JSON.stringify($('Detect User Intent').item.json.output) }}\n</intent_info_json>\n\nGoal\n- Clarify the SINGLE most ambiguous point:\n 1) Action? (search | open_link | chitchat/help)\n 2) Keyword/target? (keyword)\n 3) Platform? (reddit | n8n community | both)\n 4) Time range? (today | this week | this month | this year | all)\n 5) Sorting? (hot/new/top/comments | latest/likes/views/latest_topic/votes)\n- Turn the clarification into click-ready choices, minimize typing.\n\nResponse rules\n- Follow the language of user_message (default: Vietnamese).\n- Friendly, concise, light Gen-Z tone; ≤ 2 emojis.\n- Do NOT mention “confidence” or internal system details.\n- Do NOT invent links/titles if base lacks them.\n- If intent_info_json already says chitchat/datetime/help and includes a ready reply → send that brief reply, don’t ask further.\n- If most info is missing → ask for keyword first.\n\nOutput format (required)\n- Return a SINGLE valid Telegram HTML string:\n - Allowed tags: <b>, <i>, <u>, <a href=\"…\">, <code>, <pre>, <blockquote>, <br>\n - First line must be a short bold label.\n - Use the bullet “• ” for options (do not use <ul><li>).\n\nHeuristic to pick the clarification focus\n- Missing/uncertain keyword → ask keyword first.\n- Have keyword but missing platform → ask platform.\n- Have platform but missing time/sort → ask time first, then sort.\n- If user said “view details” but link is empty and only context_ref exists → ask to confirm result #n or request the link.\n\nSample responses\n1) Clarify action (search vs open_link)\n<b>🧐 Quick check</b><br>\nDo you want to <b>search posts</b> about <i>webhook signature</i> or <b>open details</b> of a specific post?<br>\n• Search latest posts<br>\n• Open details of item #2<br>\n• Neither of these\n\n2) Choose platform\n<b>🔎 Where should I search?</b><br>\nPick the platform:<br>\n• Reddit r/n8n<br>\n• n8n Community<br>\n• Both platforms\n\n3) Choose time range\n<b>⏱️ Which time range?</b><br>\n• Today/This week<br>\n• This month/This year<br>\n• All time\n\n4) Missing keyword\n<b>✍️ What topic do you want?</b><br>\nExamples: <i>Telegram Trigger 429</i>, <i>Google Sheets append row</i>, <i>HTTP Request OAuth2</i><br>\n• Telegram Trigger 429<br>\n• Webhook signature verification<br>\n• I’ll type another keyword\n\n5) View details but missing link\n<b>🔗 Which post should I open?</b><br>\n• Open item #2 from the previous list<br>\n• I’ll send a link to open<br>\n• Nah, go back to an overview search\n\nWorkflow execution hints\n- This node runs only when confidence < 0.7.\n- After the user picks one option, update intent/params accordingly, then jump to the main action branch (search/open_link).\n- If the user is silent > 60s, send one gentle reminder with 2 short options.\n\nRequired output\n- Send ONLY a Telegram HTML string (no JSON, no extra explanation).\n"
},
"promptType": "define"
},
"typeVersion": 2.2
},
{
"id": "396ab273-1d0b-4f79-be29-7dbf151b1c21",
"name": "AI 摘要器深入探索",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
-1216,
-256
],
"parameters": {
"text": "={{ $('Telegram Trigger - User Message').item.json.message.text }}",
"options": {
"systemMessage": "=## Role\nYou are **Ken**, assistant to Nguyễn Thiệu Toàn. Your job: read the boss’s request (my **bro**) + the provided base info (base_info_json) and craft a reply **ONLY** from that data. No web browsing, no made-up facts. In every reply, speak in first person as **“tui”** and address the boss as **“bro”**, showing playful admiration (while staying professional).\n\n## Inputs\n- user_message: the latest message from bro Toàn.\n- base_info_json: the injected data from n8n (do not infer beyond this). This may simply be an intent analysis if bro only greeted or chit-chatted.\n<base_info_json>\n{{ JSON.stringify($('Wrap as Data Object').item.json.data) }}\n</base_info_json>\n\nIntent & response modes\n1) Search overview:\n - base_info_json contains a LIST of results (title, platform, short summary, time, URL).\n - Goal: quickly summarize 3–6 notable items, highlight key points & suggest next steps.\n2) Deep-dive on a single post:\n - base_info_json contains EXTRACTED CONTENT (post + replies) with author & commenters.\n - Goal: structured summary, key insights from post & replies, conclusions/actionable steps.\n3) Chitchat:\n - If bro is just greeting/asking what the bot can do: friendly, short guidance.\n4) Missing/insufficient data:\n - Say exactly what is missing and suggest running search again or providing a URL / result #N.\n\nStyle\n- Friendly, upbeat, Gen-Z, concise, pragmatic. Emojis allowed but ≤ 2 per block.\n- Speak as **tui**; call bro **bro**. Playful, energetic, a bit cheeky—but helpful.\n- Default Vietnamese; if bro writes in another language, reply in that language.\n- Avoid long jargon; focus on “get it done”.\n\nOutput format (Telegram HTML, required)\n- Only use Telegram-supported HTML tags: <b>, <i>, <u>, <a href=\"...\">, <code>, <pre>, <blockquote>.\n- No Markdown, no JSON, no text outside HTML.\n- First line is a short bold heading. Use the bullet “• ” for lists (do not use <ul><li>).\n\nData & safety rules\n- Quote only content present in <base_info_json>. No invented URLs, no assumptions.\n- If a field is missing (e.g., post time), write “(unknown)” instead of guessing.\n- Link to the original when a URL is available; otherwise say “no link in data”.\n- You may quote short snippets via <blockquote>…</blockquote> (≤ 2–3 sentences each).\n- Add a brief freshness note if the info seems old (based on timestamps in base).\n- You always provide detail information on each topic: post time, author, number of vote/like, number of post/comment...\n\nSample response for search overview (always attach links to searchable results)\n<b>🔎 Quick roundup on Webhook signature in n8n</b>\n• <b>[n8n Community]</b> <a href=\"https://community.n8n.io/t/example-post-12345\">Verify Webhook HMAC</a> — How to set the secret and compare HMAC signatures (2025-08-12)\n• <b>[Reddit]</b> <a href=\"https://reddit.com/r/n8n/comments/abc123/example\">Best practices for webhook signature</a> — Discussion on timestamps & replay protection (2025-07-30)\n• <b>[n8n Community]</b> <a href=\"https://community.n8n.io/t/example-post-67890\">Troubleshoot invalid signature</a> — Common pitfalls & logging tips (2025-06-18)\n<i>Heads-up:</i> Bro want tui to deep-dive #1/#2 or filter by “top/this month”? 🧭\n\nSample response (deep-dive)\n<b>🧩 Deep-dive: Verify Webhook HMAC in n8n</b>\n<i>Platform:</i> n8n Community · <i>Author:</i> alice_dev · <i>Time:</i> 2025-08-12\n<blockquote>“After enabling ‘Enable Signature Verification’, set the secret and compare the signature header with the value computed from body + timestamp.”</blockquote>\n<b>Summary:</b> Post explains enabling signature verification for the Webhook node and computing HMAC over the raw body + timestamp to prevent replay. Discussion centers on signing order, time skew handling, and logging when payload changes.\n<b>Notables:</b>\n• Check time skew within ±5 minutes for safety<br>\n• Log raw body before parsing for accurate HMAC<br>\n• Disable client auto-format if it mutates payload\n<b>Takeaways:</b>\n• Enforce timestamp & nonce — reduces replay risk<br>\n• Compare signature over raw body — avoids serialize drift\n<i>Original:</i> <a href=\"https://community.n8n.io/t/example-post-12345\">community.n8n.io</a>\n\nPresentation rules\n- Length: tune to the ask, but keep it short and punchy.\n- Structure: heading → 1-line summary → bullets → CTA.\n- If many items are near-duplicates, group/compare briefly (“(similar content)”).\n\nWhen data doesn’t match intent\n- Bro wants a deep-dive but base has only a LIST → prompt: “Pick #n or send a URL so I can dive in.”\n- Bro wants overview but base has only 1 item → summarize that item (mini deep-dive) + suggest expanding the search.\n\nRequired output\n- Return a SINGLE valid Telegram HTML string.\n- No technical prefixes/suffixes, no extra explanations beyond the reply itself.\n"
},
"promptType": "define"
},
"typeVersion": 2.2
},
{
"id": "42a6e284-e6d5-4776-ac1c-7ae16daa1426",
"name": "AI 摘要器概览",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
-4048,
-944
],
"parameters": {
"text": "[Yêu cầu tự động] Tổng hợp tin hot nhất trong ngày gửi tới người dùng",
"options": {
"systemMessage": "=## Role\nYou are **Ken**, assistant to Nguyễn Thiệu Toàn. Your job: read the boss’s request (my **bro**) + the provided base info (base_info_json) and craft a reply **ONLY** from that data. No web browsing, no made-up facts. In every reply, speak in first person as **“tui”** and address the boss as **“bro”**, showing playful admiration (while staying professional).\n\n## Inputs\n- user_message: the latest message from bro Toàn.\n- base_info_json: the injected data from n8n (do not infer beyond this). This may simply be an intent analysis if bro only greeted or chit-chatted.\n<base_info_json>\n{{ JSON.stringify($('Wrap as Data Object2').item.json.data) }}\n</base_info_json>\n\nIntent & response modes\n1) Search overview:\n - base_info_json contains a LIST of results (title, platform, short summary, time, URL).\n - Goal: quickly summarize 3–6 notable items, highlight key points & suggest next steps.\n2) Deep-dive on a single post:\n - base_info_json contains EXTRACTED CONTENT (post + replies) with author & commenters.\n - Goal: structured summary, key insights from post & replies, conclusions/actionable steps.\n3) Chitchat:\n - If bro is just greeting/asking what the bot can do: friendly, short guidance.\n4) Missing/insufficient data:\n - Say exactly what is missing and suggest running search again or providing a URL / result #N.\n\nStyle\n- Friendly, upbeat, Gen-Z, concise, pragmatic. Emojis allowed but ≤ 2 per block.\n- Speak as **tui**; call bro **bro**. Playful, energetic, a bit cheeky—but helpful.\n- Default Vietnamese; if bro writes in another language, reply in that language.\n- Avoid long jargon; focus on “get it done”.\n\nOutput format (Telegram HTML, required)\n- Only use Telegram-supported HTML tags: <b>, <i>, <u>, <a href=\"...\">, <code>, <pre>, <blockquote>.\n- No Markdown, no JSON, no text outside HTML.\n- First line is a short bold heading. Use the bullet “• ” for lists (do not use <ul><li>).\n\nData & safety rules\n- Quote only content present in <base_info_json>. No invented URLs, no assumptions.\n- If a field is missing (e.g., post time), write “(unknown)” instead of guessing.\n- Link to the original when a URL is available; otherwise say “no link in data”.\n- You may quote short snippets via <blockquote>…</blockquote> (≤ 2–3 sentences each).\n- Add a brief freshness note if the info seems old (based on timestamps in base).\n\nSample response for search overview (always attach links to searchable results)\n<b>🔎 Quick roundup on Webhook signature in n8n</b>\n• <b>[n8n Community]</b> <a href=\"https://community.n8n.io/t/example-post-12345\">Verify Webhook HMAC</a> — How to set the secret and compare HMAC signatures (2025-08-12)\n• <b>[Reddit]</b> <a href=\"https://reddit.com/r/n8n/comments/abc123/example\">Best practices for webhook signature</a> — Discussion on timestamps & replay protection (2025-07-30)\n• <b>[n8n Community]</b> <a href=\"https://community.n8n.io/t/example-post-67890\">Troubleshoot invalid signature</a> — Common pitfalls & logging tips (2025-06-18)\n<i>Heads-up:</i> Bro want tui to deep-dive #1/#2 or filter by “top/this month”? 🧭\n\nSample response (deep-dive)\n<b>🧩 Deep-dive: Verify Webhook HMAC in n8n</b>\n<i>Platform:</i> n8n Community · <i>Author:</i> alice_dev · <i>Time:</i> 2025-08-12\n<blockquote>“After enabling ‘Enable Signature Verification’, set the secret and compare the signature header with the value computed from body + timestamp.”</blockquote>\n<b>Summary:</b> Post explains enabling signature verification for the Webhook node and computing HMAC over the raw body + timestamp to prevent replay. Discussion centers on signing order, time skew handling, and logging when payload changes.\n<b>Notables:</b>\n• Check time skew within ±5 minutes for safety<br>\n• Log raw body before parsing for accurate HMAC<br>\n• Disable client auto-format if it mutates payload\n<b>Takeaways:</b>\n• Enforce timestamp & nonce — reduces replay risk<br>\n• Compare signature over raw body — avoids serialize drift\n<i>Original:</i> <a href=\"https://community.n8n.io/t/example-post-12345\">community.n8n.io</a>\n\nPresentation rules\n- Length: tune to the ask, but keep it short and punchy.\n- Structure: heading → 1-line summary → bullets → CTA.\n- If many items are near-duplicates, group/compare briefly (“(similar content)”).\n\nWhen data doesn’t match intent\n- Bro wants a deep-dive but base has only a LIST → prompt: “Pick #n or send a URL so I can dive in.”\n- Bro wants overview but base has only 1 item → summarize that item (mini deep-dive) + suggest expanding the search.\n\nRequired output\n- Return a SINGLE valid Telegram HTML string.\n- No technical prefixes/suffixes, no extra explanations beyond the reply itself.\n"
},
"promptType": "define"
},
"typeVersion": 2.2
},
{
"id": "cd19593f-3229-4620-b606-ac2b9c688d73",
"name": "为 Telegram 输出格式化",
"type": "n8n-nodes-base.code",
"position": [
-928,
-256
],
"parameters": {
"jsCode": "/**\n * Telegram HTML Normalizer + Chunker (≤ 2000 chars)\n * - Markdown → Telegram HTML (b,i,u,s,code,pre,a,br,blockquote)\n * - Map/strip unsupported tags, sanitize attributes\n * - Preserve links/code/pre; do NOT split inside <a>/<pre>/<blockquote> and inline pairs\n * - Close/reopen ONLY long-lived tags per chunk (a, pre, blockquote)\n * - Prevent stray closing tags and treat lone \"<\" safely\n */\n\nconst MAX_LEN = 2000;\n\n/* === Get input === */\nlet text = ($input.first().json.output ?? $input.first().json.text ?? '').trim();\nif (!text) return [];\n\n/* === Allowed Telegram tags === */\nconst allowed = new Set(['a','b','i','u','s','code','pre','br','blockquote']);\n\n/* ---------- Utils ---------- */\nconst escapeHtml = (s) => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n\nfunction sanitizeHref(href){\n if (!href) return '';\n const h = String(href).trim().replace(/^['\"]|['\"]$/g,'');\n const lower = h.toLowerCase();\n if (lower.startsWith('javascript:') || lower.startsWith('data:')) return '';\n return h;\n}\n\n/* ---------- 1) Markdown -> HTML ---------- */\nfunction mdToHtml(md){\n let s = md;\n // code block\n s = s.replace(/```([\\s\\S]*?)```/g, (m,p1)=>`<pre>${escapeHtml(p1.trim())}</pre>`);\n // inline code\n s = s.replace(/`([^`]+)`/g, (m,p1)=>`<code>${escapeHtml(p1)}</code>`);\n // bold / italic / underline / strike\n s = s.replace(/\\*\\*([^*]+)\\*\\*/g, '<b>$1</b>');\n s = s.replace(/(^|[\\s(])\\*([^*\\n]+)\\*(?=$|[\\s).,!?\\]])/g, '$1<i>$2</i>');\n s = s.replace(/__([^_]+)__/g, '<u>$1</u>');\n s = s.replace(/~~([^~]+)~~/g, '<s>$1</s>');\n // headers → bold + newline\n s = s.replace(/^(#{1,6})\\s+(.+)$/gm, (m, hashes, content)=>`<b>${content.trim()}</b>\\n`);\n // list markers → bullet\n s = s.replace(/^[\\-\\*\\+]\\s+(.+)$/gm, '• $1');\n // [text](url)\n s = s.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, (m,txt,url)=>{\n const safe = sanitizeHref(url);\n return safe ? `<a href=\"${safe}\">${txt}</a>` : txt;\n });\n return s;\n}\n\n/* ---------- 2) Protect <pre>/<code> contents ---------- */\nfunction protectPreCodeBlocks(input){\n const placeholders = [];\n let out = input;\n\n out = out.replace(/<pre\\b[^>]*>([\\s\\S]*?)<\\/pre>/gi, (m, inner)=>{\n const token = `__PRE_BLOCK_${placeholders.length}__`;\n placeholders.push({token, tag:'pre', content: escapeHtml(inner)});\n return token;\n });\n\n out = out.replace(/<code\\b[^>]*>([\\s\\S]*?)<\\/code>/gi, (m, inner)=>{\n const token = `__CODE_BLOCK_${placeholders.length}__`;\n placeholders.push({token, tag:'code', content: escapeHtml(inner)});\n return token;\n });\n\n const restore = (s)=>{\n for (const ph of placeholders){\n s = s.split(ph.token).join(`<${ph.tag}>${ph.content}</${ph.tag}>`);\n }\n return s;\n };\n\n return { text: out, restore };\n}\n\n/* ---------- 3) HTML normalize ---------- */\nfunction htmlNormalize(input){\n let s = input;\n\n s = s.replace(/<br\\s*\\/?>/gi, '<br>');\n s = s.replace(/<li[^>]*>/gi, '• ').replace(/<\\/li>/gi, '\\n');\n s = s.replace(/<\\/?(ul|ol)[^>]*>/gi, '');\n\n s = s.replace(/<p[^>]*>/gi, '').replace(/<\\/p>/gi, '\\n\\n');\n s = s.replace(/<div[^>]*>/gi, '').replace(/<\\/div>/gi, '\\n');\n\n s = s.replace(/<h[1-6][^>]*>([\\s\\S]*?)<\\/h[1-6]>/gi, (m, inner)=>`<b>${inner.trim()}</b>\\n`);\n\n // Map aliases -> canonical\n s = s.replace(/<\\/?strong\\b/gi, t=>t.replace(/strong/i,'b'));\n s = s.replace(/<\\/?em\\b/gi, t=>t.replace(/em/i,'i'));\n s = s.replace(/<\\/?ins\\b/gi, t=>t.replace(/ins/i,'u'));\n s = s.replace(/<\\/?(strike|del)\\b/gi, t=>t.replace(/strike|del/i,'s'));\n\n // remove spans\n s = s.replace(/<\\/?span[^>]*>/gi, '');\n\n // images -> textual hint\n s = s.replace(/<img[^>]*src\\s*=\\s*(\"[^\"]+\"|'[^']+'|[^\\s>]+)[^>]*>/gi, (m,src)=>{\n const clean = String(src).replace(/^['\"]|['\"]$/g,'');\n return ` (ảnh: ${clean}) `;\n });\n\n return s;\n}\n\n/* ---------- 3.5) Escape lone '<' that are NOT valid tags ---------- */\nfunction escapeLoneAngles(input){\n // allow only tags in: a|b|i|u|s|code|pre|br|blockquote\n return input.replace(/<(?!\\/?(a|b|i|u|s|code|pre|br|blockquote)\\b)/gi, '<');\n}\n\n/* ---------- 4) Sanitize tags (keep only Telegram tags) ---------- */\nfunction sanitizeHtml(input){\n return input.replace(/<\\/?([a-z0-9]+)(\\s+[^>]*)?>/gi, (full, tag, attrs='')=>{\n const t = tag.toLowerCase();\n\n if (t === 'a'){\n if (full.startsWith('</')) return '</a>';\n const hrefMatch = attrs.match(/href\\s*=\\s*(\"[^\"]+\"|'[^']+'|[^\\s>]+)/i);\n const safeHref = hrefMatch ? sanitizeHref(hrefMatch[1]) : '';\n return safeHref ? `<a href=\"${safeHref}\">` : '';\n }\n\n if (!allowed.has(t)) return '';\n if (t === 'br') return '<br>';\n return full[1] === '/' ? `</${t}>` : `<${t}>`;\n });\n}\n\n/* ---------- 5) Track ONLY long-lived tags across chunks ---------- */\n/* We only track <a>, <pre>, <blockquote> so inline pairs never get auto-closed. */\nfunction getOpenTagsWithAttrs(html){\n const trackable = new Set(['a','pre','blockquote']);\n const stack = [];\n const re = /<\\/?([a-z0-9]+)(?:\\s+[^>]*)?>/gi;\n let m;\n while ((m = re.exec(html))){\n const raw = m[0];\n const t = m[1].toLowerCase();\n if (!trackable.has(t)) continue;\n\n if (raw[1] === '/'){\n const idx = [...stack].reverse().findIndex(e=>e.tag===t);\n if (idx !== -1) stack.splice(stack.length - 1 - idx, 1);\n } else {\n if (t === 'a'){\n const hrefMatch = raw.match(/href\\s*=\\s*(\"[^\"]+\"|'[^']+'|[^\\s>]+)/i);\n const href = hrefMatch ? sanitizeHref(hrefMatch[1]) : '';\n if (href) stack.push({tag:'a', href});\n } else {\n stack.push({tag:t});\n }\n }\n }\n return stack;\n}\nconst closeTags = (open)=>[...open].reverse().map(e=>`</${e.tag}>`).join('');\nconst openTagsStr = (open)=>open.map(e=>e.tag==='a'?`<a href=\"${e.href}\">`:`<${e.tag}>`).join('');\n\n/* ---------- 6) Normalize pipeline ---------- */\nfunction normalizeToTelegramHtml(input){\n const looksLikeMd = /(^|\\s)[*_`~]|^#{1,6}\\s|```/.test(input);\n let s = looksLikeMd ? mdToHtml(input) : input;\n\n s = htmlNormalize(s);\n\n const protector = protectPreCodeBlocks(s);\n s = protector.text;\n\n // avoid \"<10\" etc. being treated as a tag\n s = escapeLoneAngles(s);\n\n s = sanitizeHtml(s);\n s = protector.restore(s);\n\n s = s.replace(/\\n{3,}/g, '\\n\\n').trim();\n return s;\n}\n\n/* ---------- 7) Split safely ---------- */\nfunction splitHtmlSmart(html, maxLen){\n // Atomic segments: a, pre, blockquote, and inline pairs b/i/u/s/code\n const re = /(<a\\b[^>]*>[\\s\\S]*?<\\/a>)|(<pre\\b[^>]*>[\\s\\S]*?<\\/pre>)|(<blockquote\\b[^>]*>[\\s\\S]*?<\\/blockquote>)|(<b\\b[^>]*>[\\s\\S]*?<\\/b>)|(<i\\b[^>]*>[\\s\\S]*?<\\/i>)|(<u\\b[^>]*>[\\s\\S]*?<\\/u>)|(<s\\b[^>]*>[\\s\\S]*?<\\/s>)|(<code\\b[^>]*>[\\s\\S]*?<\\/code>)|(<br\\s*\\/?>)|(<[^>]+>)|([^<]+)/gi;\n\n const segments = [];\n let m;\n while ((m = re.exec(html))){\n segments.push(\n m[1] || m[2] || m[3] || m[4] || m[5] || m[6] || m[7] || m[8] ||\n (m[9] ? '<br>' : (m[10] || m[11]))\n );\n }\n\n const chunks = [];\n let buffer = '';\n let prefixOpen = '';\n\n const pushBuffer = ()=>{\n if (!buffer.trim()) return;\n let out = prefixOpen + buffer;\n // Only close a/pre/blockquote\n const open = getOpenTagsWithAttrs(out);\n out += closeTags(open);\n prefixOpen = openTagsStr(open); // reopen at next chunk if any\n chunks.push(out.trim());\n buffer = '';\n };\n\n // Shrink visible text inside <a> if needed\n const shrinkAnchorIfTooLong = (seg, room)=>{\n const mm = /^<a\\b[^>]*>([\\s\\S]*?)<\\/a>$/i.exec(seg);\n if (!mm) return seg;\n const visible = mm[1];\n if (seg.length <= room) return seg;\n const truncated = (visible.length > room - 30) ? (visible.slice(0, Math.max(3, room - 33)) + '...') : visible;\n return seg.replace(visible, truncated);\n };\n\n // Split huge <pre> / <blockquote> into multiple wrapped chunks\n const splitWrappedBlock = (segTag, seg, available, emit)=>{\n const rx = new RegExp(`^<${segTag}[^>]*>([\\\\s\\\\S]*)<\\\\/${segTag}>$`, 'i');\n const mm = rx.exec(seg);\n if (!mm) return false;\n let content = mm[1];\n const shellLen = (`<${segTag}></${segTag}>`).length; // 11 for pre, 23 for blockquote, etc.\n\n // if it fits already → no split here\n if (seg.length <= available) return false;\n\n // flush current buffer to start fresh\n if (buffer.trim()) pushBuffer();\n\n while (content.length){\n // recompute room each loop (prefixOpen may change)\n const room = Math.max(200, maxLen - prefixOpen.length - shellLen - 10);\n const slice = content.slice(0, room);\n buffer = `<${segTag}>${slice}</${segTag}>`;\n pushBuffer();\n content = content.slice(slice.length);\n }\n return true;\n };\n\n for (const seg of segments){\n const segLen = seg.length;\n\n // if appending seg would overflow\n if ((prefixOpen.length + buffer.length + segLen) > maxLen){\n\n // 7.1 Try shrinking anchor text\n if (/^<a\\b/i.test(seg)){\n const room = maxLen - prefixOpen.length - buffer.length - 1;\n const shrunk = shrinkAnchorIfTooLong(seg, Math.max(60, room));\n if ((prefixOpen.length + buffer.length + shrunk.length) <= maxLen){\n buffer += shrunk;\n continue;\n }\n }\n\n // 7.2 Split huge <pre>/<blockquote> safely (keep wrappers)\n const available = maxLen - prefixOpen.length - buffer.length;\n if (/^<pre\\b/i.test(seg)){\n if (splitWrappedBlock('pre', seg, available)) continue;\n }\n if (/^<blockquote\\b/i.test(seg)){\n if (splitWrappedBlock('blockquote', seg, available)) continue;\n }\n\n // 7.3 Push current chunk\n pushBuffer();\n\n // 7.4 Place seg into new buffer or split plain text / hard-cut tag as last resort\n if ((prefixOpen.length + segLen) <= maxLen){\n buffer = seg;\n } else if (!seg.startsWith('<')) {\n // split plain text by whitespace then hard-cut if needed\n const words = seg.split(/(\\s+)/);\n for (const w of words){\n const candidate = buffer + w;\n if ((prefixOpen.length + candidate.length) > maxLen){\n pushBuffer();\n if ((prefixOpen.length + w.length) > maxLen){\n let s = w;\n const room = Math.max(200, maxLen - prefixOpen.length - 10);\n while (s.length){\n buffer = s.slice(0, room);\n pushBuffer();\n s = s.slice(room);\n }\n } else {\n buffer = w;\n }\n } else {\n buffer = candidate;\n }\n }\n } else {\n // FINAL FALLBACK: extremely long single tag (very rare)\n // Hard-cut by characters (may break styling but keeps length constraints)\n let start = 0;\n const room = Math.max(200, maxLen - prefixOpen.length - 10);\n while (start < segLen){\n buffer = seg.slice(start, start + room);\n pushBuffer();\n start += room;\n }\n }\n continue;\n }\n\n // fits → append\n buffer += seg;\n }\n\n if (buffer.trim()) pushBuffer();\n return chunks;\n}\n\n/* ===== Run ===== */\nconst normalized = normalizeToTelegramHtml(text);\nconst chunks = splitHtmlSmart(normalized, MAX_LEN);\n\n/* Export to Telegram node */\nreturn chunks.map(c => ({ json: { text: c } }));\n"
},
"typeVersion": 2
},
{
"id": "39ffde34-51f1-4463-9bf6-a7ca9410b34b",
"name": "清理和分块",
"type": "n8n-nodes-base.code",
"position": [
-3680,
-944
],
"parameters": {
"jsCode": "/**\n * Telegram HTML Normalizer + Chunker (≤ 2000 chars)\n * - Markdown → Telegram HTML (b,i,u,s,code,pre,a,br,blockquote)\n * - Map/strip unsupported tags, sanitize attributes\n * - Preserve links/code/pre; do NOT split inside <a>/<pre>/<blockquote> and inline pairs\n * - Close/reopen ONLY long-lived tags per chunk (a, pre, blockquote)\n * - Prevent stray closing tags and treat lone \"<\" safely\n */\n\nconst MAX_LEN = 2000;\n\n/* === Get input === */\nlet text = ($input.first().json.output ?? $input.first().json.text ?? '').trim();\nif (!text) return [];\n\n/* === Allowed Telegram tags === */\nconst allowed = new Set(['a','b','i','u','s','code','pre','br','blockquote']);\n\n/* ---------- Utils ---------- */\nconst escapeHtml = (s) => String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');\n\nfunction sanitizeHref(href){\n if (!href) return '';\n const h = String(href).trim().replace(/^['\"]|['\"]$/g,'');\n const lower = h.toLowerCase();\n if (lower.startsWith('javascript:') || lower.startsWith('data:')) return '';\n return h;\n}\n\n/* ---------- 1) Markdown -> HTML ---------- */\nfunction mdToHtml(md){\n let s = md;\n // code block\n s = s.replace(/```([\\s\\S]*?)```/g, (m,p1)=>`<pre>${escapeHtml(p1.trim())}</pre>`);\n // inline code\n s = s.replace(/`([^`]+)`/g, (m,p1)=>`<code>${escapeHtml(p1)}</code>`);\n // bold / italic / underline / strike\n s = s.replace(/\\*\\*([^*]+)\\*\\*/g, '<b>$1</b>');\n s = s.replace(/(^|[\\s(])\\*([^*\\n]+)\\*(?=$|[\\s).,!?\\]])/g, '$1<i>$2</i>');\n s = s.replace(/__([^_]+)__/g, '<u>$1</u>');\n s = s.replace(/~~([^~]+)~~/g, '<s>$1</s>');\n // headers → bold + newline\n s = s.replace(/^(#{1,6})\\s+(.+)$/gm, (m, hashes, content)=>`<b>${content.trim()}</b>\\n`);\n // list markers → bullet\n s = s.replace(/^[\\-\\*\\+]\\s+(.+)$/gm, '• $1');\n // [text](url)\n s = s.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, (m,txt,url)=>{\n const safe = sanitizeHref(url);\n return safe ? `<a href=\"${safe}\">${txt}</a>` : txt;\n });\n return s;\n}\n\n/* ---------- 2) Protect <pre>/<code> contents ---------- */\nfunction protectPreCodeBlocks(input){\n const placeholders = [];\n let out = input;\n\n out = out.replace(/<pre\\b[^>]*>([\\s\\S]*?)<\\/pre>/gi, (m, inner)=>{\n const token = `__PRE_BLOCK_${placeholders.length}__`;\n placeholders.push({token, tag:'pre', content: escapeHtml(inner)});\n return token;\n });\n\n out = out.replace(/<code\\b[^>]*>([\\s\\S]*?)<\\/code>/gi, (m, inner)=>{\n const token = `__CODE_BLOCK_${placeholders.length}__`;\n placeholders.push({token, tag:'code', content: escapeHtml(inner)});\n return token;\n });\n\n const restore = (s)=>{\n for (const ph of placeholders){\n s = s.split(ph.token).join(`<${ph.tag}>${ph.content}</${ph.tag}>`);\n }\n return s;\n };\n\n return { text: out, restore };\n}\n\n/* ---------- 3) HTML normalize ---------- */\nfunction htmlNormalize(input){\n let s = input;\n\n s = s.replace(/<br\\s*\\/?>/gi, '<br>');\n s = s.replace(/<li[^>]*>/gi, '• ').replace(/<\\/li>/gi, '\\n');\n s = s.replace(/<\\/?(ul|ol)[^>]*>/gi, '');\n\n s = s.replace(/<p[^>]*>/gi, '').replace(/<\\/p>/gi, '\\n\\n');\n s = s.replace(/<div[^>]*>/gi, '').replace(/<\\/div>/gi, '\\n');\n\n s = s.replace(/<h[1-6][^>]*>([\\s\\S]*?)<\\/h[1-6]>/gi, (m, inner)=>`<b>${inner.trim()}</b>\\n`);\n\n // Map aliases -> canonical\n s = s.replace(/<\\/?strong\\b/gi, t=>t.replace(/strong/i,'b'));\n s = s.replace(/<\\/?em\\b/gi, t=>t.replace(/em/i,'i'));\n s = s.replace(/<\\/?ins\\b/gi, t=>t.replace(/ins/i,'u'));\n s = s.replace(/<\\/?(strike|del)\\b/gi, t=>t.replace(/strike|del/i,'s'));\n\n // remove spans\n s = s.replace(/<\\/?span[^>]*>/gi, '');\n\n // images -> textual hint\n s = s.replace(/<img[^>]*src\\s*=\\s*(\"[^\"]+\"|'[^']+'|[^\\s>]+)[^>]*>/gi, (m,src)=>{\n const clean = String(src).replace(/^['\"]|['\"]$/g,'');\n return ` (ảnh: ${clean}) `;\n });\n\n return s;\n}\n\n/* ---------- 3.5) Escape lone '<' that are NOT valid tags ---------- */\nfunction escapeLoneAngles(input){\n // allow only tags in: a|b|i|u|s|code|pre|br|blockquote\n return input.replace(/<(?!\\/?(a|b|i|u|s|code|pre|br|blockquote)\\b)/gi, '<');\n}\n\n/* ---------- 4) Sanitize tags (keep only Telegram tags) ---------- */\nfunction sanitizeHtml(input){\n return input.replace(/<\\/?([a-z0-9]+)(\\s+[^>]*)?>/gi, (full, tag, attrs='')=>{\n const t = tag.toLowerCase();\n\n if (t === 'a'){\n if (full.startsWith('</')) return '</a>';\n const hrefMatch = attrs.match(/href\\s*=\\s*(\"[^\"]+\"|'[^']+'|[^\\s>]+)/i);\n const safeHref = hrefMatch ? sanitizeHref(hrefMatch[1]) : '';\n return safeHref ? `<a href=\"${safeHref}\">` : '';\n }\n\n if (!allowed.has(t)) return '';\n if (t === 'br') return '<br>';\n return full[1] === '/' ? `</${t}>` : `<${t}>`;\n });\n}\n\n/* ---------- 5) Track ONLY long-lived tags across chunks ---------- */\n/* We only track <a>, <pre>, <blockquote> so inline pairs never get auto-closed. */\nfunction getOpenTagsWithAttrs(html){\n const trackable = new Set(['a','pre','blockquote']);\n const stack = [];\n const re = /<\\/?([a-z0-9]+)(?:\\s+[^>]*)?>/gi;\n let m;\n while ((m = re.exec(html))){\n const raw = m[0];\n const t = m[1].toLowerCase();\n if (!trackable.has(t)) continue;\n\n if (raw[1] === '/'){\n const idx = [...stack].reverse().findIndex(e=>e.tag===t);\n if (idx !== -1) stack.splice(stack.length - 1 - idx, 1);\n } else {\n if (t === 'a'){\n const hrefMatch = raw.match(/href\\s*=\\s*(\"[^\"]+\"|'[^']+'|[^\\s>]+)/i);\n const href = hrefMatch ? sanitizeHref(hrefMatch[1]) : '';\n if (href) stack.push({tag:'a', href});\n } else {\n stack.push({tag:t});\n }\n }\n }\n return stack;\n}\nconst closeTags = (open)=>[...open].reverse().map(e=>`</${e.tag}>`).join('');\nconst openTagsStr = (open)=>open.map(e=>e.tag==='a'?`<a href=\"${e.href}\">`:`<${e.tag}>`).join('');\n\n/* ---------- 6) Normalize pipeline ---------- */\nfunction normalizeToTelegramHtml(input){\n const looksLikeMd = /(^|\\s)[*_`~]|^#{1,6}\\s|```/.test(input);\n let s = looksLikeMd ? mdToHtml(input) : input;\n\n s = htmlNormalize(s);\n\n const protector = protectPreCodeBlocks(s);\n s = protector.text;\n\n // avoid \"<10\" etc. being treated as a tag\n s = escapeLoneAngles(s);\n\n s = sanitizeHtml(s);\n s = protector.restore(s);\n\n s = s.replace(/\\n{3,}/g, '\\n\\n').trim();\n return s;\n}\n\n/* ---------- 7) Split safely ---------- */\nfunction splitHtmlSmart(html, maxLen){\n // Atomic segments: a, pre, blockquote, and inline pairs b/i/u/s/code\n const re = /(<a\\b[^>]*>[\\s\\S]*?<\\/a>)|(<pre\\b[^>]*>[\\s\\S]*?<\\/pre>)|(<blockquote\\b[^>]*>[\\s\\S]*?<\\/blockquote>)|(<b\\b[^>]*>[\\s\\S]*?<\\/b>)|(<i\\b[^>]*>[\\s\\S]*?<\\/i>)|(<u\\b[^>]*>[\\s\\S]*?<\\/u>)|(<s\\b[^>]*>[\\s\\S]*?<\\/s>)|(<code\\b[^>]*>[\\s\\S]*?<\\/code>)|(<br\\s*\\/?>)|(<[^>]+>)|([^<]+)/gi;\n\n const segments = [];\n let m;\n while ((m = re.exec(html))){\n segments.push(\n m[1] || m[2] || m[3] || m[4] || m[5] || m[6] || m[7] || m[8] ||\n (m[9] ? '<br>' : (m[10] || m[11]))\n );\n }\n\n const chunks = [];\n let buffer = '';\n let prefixOpen = '';\n\n const pushBuffer = ()=>{\n if (!buffer.trim()) return;\n let out = prefixOpen + buffer;\n // Only close a/pre/blockquote\n const open = getOpenTagsWithAttrs(out);\n out += closeTags(open);\n prefixOpen = openTagsStr(open); // reopen at next chunk if any\n chunks.push(out.trim());\n buffer = '';\n };\n\n // Shrink visible text inside <a> if needed\n const shrinkAnchorIfTooLong = (seg, room)=>{\n const mm = /^<a\\b[^>]*>([\\s\\S]*?)<\\/a>$/i.exec(seg);\n if (!mm) return seg;\n const visible = mm[1];\n if (seg.length <= room) return seg;\n const truncated = (visible.length > room - 30) ? (visible.slice(0, Math.max(3, room - 33)) + '...') : visible;\n return seg.replace(visible, truncated);\n };\n\n // Split huge <pre> / <blockquote> into multiple wrapped chunks\n const splitWrappedBlock = (segTag, seg, available, emit)=>{\n const rx = new RegExp(`^<${segTag}[^>]*>([\\\\s\\\\S]*)<\\\\/${segTag}>$`, 'i');\n const mm = rx.exec(seg);\n if (!mm) return false;\n let content = mm[1];\n const shellLen = (`<${segTag}></${segTag}>`).length; // 11 for pre, 23 for blockquote, etc.\n\n // if it fits already → no split here\n if (seg.length <= available) return false;\n\n // flush current buffer to start fresh\n if (buffer.trim()) pushBuffer();\n\n while (content.length){\n // recompute room each loop (prefixOpen may change)\n const room = Math.max(200, maxLen - prefixOpen.length - shellLen - 10);\n const slice = content.slice(0, room);\n buffer = `<${segTag}>${slice}</${segTag}>`;\n pushBuffer();\n content = content.slice(slice.length);\n }\n return true;\n };\n\n for (const seg of segments){\n const segLen = seg.length;\n\n // if appending seg would overflow\n if ((prefixOpen.length + buffer.length + segLen) > maxLen){\n\n // 7.1 Try shrinking anchor text\n if (/^<a\\b/i.test(seg)){\n const room = maxLen - prefixOpen.length - buffer.length - 1;\n const shrunk = shrinkAnchorIfTooLong(seg, Math.max(60, room));\n if ((prefixOpen.length + buffer.length + shrunk.length) <= maxLen){\n buffer += shrunk;\n continue;\n }\n }\n\n // 7.2 Split huge <pre>/<blockquote> safely (keep wrappers)\n const available = maxLen - prefixOpen.length - buffer.length;\n if (/^<pre\\b/i.test(seg)){\n if (splitWrappedBlock('pre', seg, available)) continue;\n }\n if (/^<blockquote\\b/i.test(seg)){\n if (splitWrappedBlock('blockquote', seg, available)) continue;\n }\n\n // 7.3 Push current chunk\n pushBuffer();\n\n // 7.4 Place seg into new buffer or split plain text / hard-cut tag as last resort\n if ((prefixOpen.length + segLen) <= maxLen){\n buffer = seg;\n } else if (!seg.startsWith('<')) {\n // split plain text by whitespace then hard-cut if needed\n const words = seg.split(/(\\s+)/);\n for (const w of words){\n const candidate = buffer + w;\n if ((prefixOpen.length + candidate.length) > maxLen){\n pushBuffer();\n if ((prefixOpen.length + w.length) > maxLen){\n let s = w;\n const room = Math.max(200, maxLen - prefixOpen.length - 10);\n while (s.length){\n buffer = s.slice(0, room);\n pushBuffer();\n s = s.slice(room);\n }\n } else {\n buffer = w;\n }\n } else {\n buffer = candidate;\n }\n }\n } else {\n // FINAL FALLBACK: extremely long single tag (very rare)\n // Hard-cut by characters (may break styling but keeps length constraints)\n let start = 0;\n const room = Math.max(200, maxLen - prefixOpen.length - 10);\n while (start < segLen){\n buffer = seg.slice(start, start + room);\n pushBuffer();\n start += room;\n }\n }\n continue;\n }\n\n // fits → append\n buffer += seg;\n }\n\n if (buffer.trim()) pushBuffer();\n return chunks;\n}\n\n/* ===== Run ===== */\nconst normalized = normalizeToTelegramHtml(text);\nconst chunks = splitHtmlSmart(normalized, MAX_LEN);\n\n/* Export to Telegram node */\nreturn chunks.map(c => ({ json: { text: c } }));\n"
},
"typeVersion": 2
},
{
"id": "64b00ec5-0764-4742-80e6-305c9926a940",
"name": "发送输入中动作",
"type": "n8n-nodes-base.telegram",
"position": [
-3824,
-352
],
"webhookId": "0a00ab89-9035-4c14-a093-98c0b315f65a",
"parameters": {
"chatId": "={{ $('Telegram Trigger - User Message').item.json.message.from.id }}",
"operation": "sendChatAction"
},
"credentials": {
"telegramApi": {
"id": "o4Df0vGzQkRsas07",
"name": "n8n Notification Bot"
}
},
"typeVersion": 1.2
},
{
"id": "b16014a7-866e-4c51-b37a-6cd7512ddc67",
"name": "便签",
"type": "n8n-nodes-base.stickyNote",
"position": [
-4960,
-1456
],
"parameters": {
"color": 2,
"width": 1584,
"height": 800,
"content": "## 1. 每日动态自动化"
},
"typeVersion": 1
},
{
"id": "0dd1e595-4c56-417d-a06f-5eeb0435c845",
"name": "包装为数据对象2",
"type": "n8n-nodes-base.code",
"position": [
-4016,
-1168
],
"parameters": {
"jsCode": "return [\n {\n json: {\n data: items.map(item => item.json)\n }\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "bb812af8-4c1c-4800-a272-dd8a14f6b971",
"name": "设置记忆 ID 会话",
"type": "n8n-nodes-base.set",
"position": [
-3808,
-1168
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "a4d9d186-0b7e-4e65-aecf-91f400e826d8",
"name": "MyTelegramID",
"type": "number",
"value": 6163095869
}
]
}
},
"typeVersion": 3.4
}
],
"pinData": {},
"connections": {
"Reddit": {
"main": [
[
{
"node": "Get Only Search Result1",
"type": "main",
"index": 0
}
]
]
},
"Comment": {
"main": [
[
{
"node": "Get Comment",
"type": "main",
"index": 0
}
]
]
},
"Has time?": {
"main": [
[
{
"node": "Reddit Search page JSON (has time)",
"type": "main",
"index": 0
}
],
[
{
"node": "Reddit Search page (no time)",
"type": "main",
"index": 0
}
]
]
},
"Platform?": {
"main": [
[
{
"node": "n8n Community Fetch JSON",
"type": "main",
"index": 0
},
{
"node": "Has keyword?",
"type": "main",
"index": 0
}
],
[
{
"node": "Has keyword?",
"type": "main",
"index": 0
}
],
[
{
"node": "n8n Community Fetch JSON",
"type": "main",
"index": 0
}
]
]
},
"If Reddit?": {
"main": [
[
{
"node": "Get Post Content",
"type": "main",
"index": 0
}
]
]
},
"Get Comment": {
"main": [
[
{
"node": "Comment Summary 1",
"type": "main",
"index": 0
}
]
]
},
"Get Comment1": {
"main": [
[
{
"node": "Comment Summary 2",
"type": "main",
"index": 0
}
]
]
},
"Has keyword?": {
"main": [
[
{
"node": "Reddit Search page JSON (no keyword)",
"type": "main",
"index": 0
}
],
[
{
"node": "Has time?",
"type": "main",
"index": 0
}
]
]
},
"Merge Sources1": {
"main": [
[
{
"node": "Wrap as Data Object2",
"type": "main",
"index": 0
}
]
]
},
"Clean and Chunk": {
"main": [
[
{
"node": "Send Auto Reply",
"type": "main",
"index": 0
}
]
]
},
"Groq Chat Model": {
"ai_languageModel": [
[
{
"node": "Detect User Intent",
"type": "ai_languageModel",
"index": 0
},
{
"node": "Structured Output Parser",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Branch by Intent": {
"main": [
[
{
"node": "Platform?",
"type": "main",
"index": 0
}
],
[
{
"node": "Wrap as Data Object",
"type": "main",
"index": 0
}
],
[
{
"node": "Open link Reddit or n8n Comunity?",
"type": "main",
"index": 0
}
],
[
{
"node": "AI Summarizer Clarify",
"type": "main",
"index": 0
}
]
]
},
"Clean and Chunk2": {
"main": [
[
{
"node": "Send reply1",
"type": "main",
"index": 0
}
]
]
},
"Get Post Content": {
"main": [
[
{
"node": "Comment",
"type": "main",
"index": 0
},
{
"node": "Merge Link Content",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Reddit",
"type": "main",
"index": 0
},
{
"node": "n8n Community Fetch",
"type": "main",
"index": 0
}
]
]
},
"Comment Summary 1": {
"main": [
[
{
"node": "Merge Link Content",
"type": "main",
"index": 1
}
]
]
},
"Comment Summary 2": {
"main": [
[
{
"node": "Merge Link Content",
"type": "main",
"index": 1
}
]
]
},
"Get Topic Content": {
"main": [
[
{
"node": "Merge Link Content",
"type": "main",
"index": 0
}
]
]
},
"If n8n Community?": {
"main": [
[
{
"node": "Get Topic Content",
"type": "main",
"index": 0
},
{
"node": "Get Comment1",
"type": "main",
"index": 0
}
]
]
},
"Detect User Intent": {
"main": [
[
{
"node": "Branch by Intent",
"type": "main",
"index": 0
},
{
"node": "Send Typing Action",
"type": "main",
"index": 0
}
]
]
},
"Merge Link Content": {
"main": [
[
{
"node": "Wrap as Data Object",
"type": "main",
"index": 0
}
]
]
},
"Merge Search Result": {
"main": [
[
{
"node": "Wrap as Data Object",
"type": "main",
"index": 0
}
]
]
},
"MongoDB Chat Memory": {
"ai_memory": [
[
{
"node": "Detect User Intent",
"type": "ai_memory",
"index": 0
}
]
]
},
"Wrap as Data Object": {
"main": [
[
{
"node": "AI Summarizer Deep-dive",
"type": "main",
"index": 0
}
]
]
},
"n8n Community Fetch": {
"main": [
[
{
"node": "Get Only Search Result2",
"type": "main",
"index": 0
}
]
]
},
"MongoDB Chat Memory1": {
"ai_memory": [
[
{
"node": "AI Summarizer Deep-dive",
"type": "ai_memory",
"index": 0
}
]
]
},
"MongoDB Chat Memory2": {
"ai_memory": [
[
{
"node": "AI Summarizer Overview",
"type": "ai_memory",
"index": 0
}
]
]
},
"MongoDB Chat Memory3": {
"ai_memory": [
[
{
"node": "AI Summarizer Clarify",
"type": "ai_memory",
"index": 0
}
]
]
},
"Wrap as Data Object2": {
"main": [
[
{
"node": "Set Memory ID Session",
"type": "main",
"index": 0
}
]
]
},
"AI Summarizer Clarify": {
"main": [
[
{
"node": "Clean and Chunk2",
"type": "main",
"index": 0
}
]
]
},
"Set Memory ID Session": {
"main": [
[
{
"node": "AI Summarizer Overview",
"type": "main",
"index": 0
}
]
]
},
"AI Summarizer Overview": {
"main": [
[
{
"node": "Clean and Chunk",
"type": "main",
"index": 0
}
]
]
},
"AI Summarizer Deep-dive": {
"main": [
[
{
"node": "Format for Telegram Output",
"type": "main",
"index": 0
}
]
]
},
"Get Only Search Result1": {
"main": [
[
{
"node": "Merge Sources1",
"type": "main",
"index": 0
}
]
]
},
"Get Only Search Result2": {
"main": [
[
{
"node": "Merge Sources1",
"type": "main",
"index": 1
}
]
]
},
"Google Gemini Chat Model": {
"ai_languageModel": [
[
{
"node": "AI Summarizer Deep-dive",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Structured Output Parser": {
"ai_outputParser": [
[
{
"node": "Detect User Intent",
"type": "ai_outputParser",
"index": 0
}
]
]
},
"n8n Community Fetch JSON": {
"main": [
[
{
"node": "Get Only n8n community Search Result",
"type": "main",
"index": 0
}
]
]
},
"Google Gemini Chat Model1": {
"ai_languageModel": [
[
{
"node": "AI Summarizer Overview",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Google Gemini Chat Model2": {
"ai_languageModel": [
[
{
"node": "AI Summarizer Clarify",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Format for Telegram Output": {
"main": [
[
{
"node": "Send reply",
"type": "main",
"index": 0
}
]
]
},
"Reddit Search page (no time)": {
"main": [
[
{
"node": "Get Only Reddit Search Result",
"type": "main",
"index": 0
}
]
]
},
"Get Only Reddit Search Result": {
"main": [
[
{
"node": "Merge Search Result",
"type": "main",
"index": 0
}
]
]
},
"Telegram Trigger - User Message": {
"main": [
[
{
"node": "Detect User Intent",
"type": "main",
"index": 0
}
]
]
},
"Open link Reddit or n8n Comunity?": {
"main": [
[
{
"node": "If Reddit?",
"type": "main",
"index": 0
}
],
[
{
"node": "If n8n Community?",
"type": "main",
"index": 0
}
]
]
},
"Reddit Search page JSON (has time)": {
"main": [
[
{
"node": "Get Only Reddit Search Result",
"type": "main",
"index": 0
}
]
]
},
"Get Only n8n community Search Result": {
"main": [
[
{
"node": "Merge Search Result",
"type": "main",
"index": 1
}
]
]
},
"Reddit Search page JSON (no keyword)": {
"main": [
[
{
"node": "Get Only Reddit Search Result",
"type": "main",
"index": 0
}
]
]
}
}
}常见问题
如何使用这个工作流?
复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。
这个工作流适合什么场景?
这是一个高级难度的工作流,适用于Miscellaneous、AI Chatbot、Multimodal AI等场景。适合高级用户,包含 16+ 个节点的复杂工作流
需要付费吗?
本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。
相关工作流推荐
多智能体Telegram机器人
使用Telegram和Google Gemini的多智能体个人助手机器人
Set
Code
Merge
+18
85 节点Akil A
Miscellaneous
使用Gemini AI视觉分析与Telegram警报监控X平台品牌提及
使用Gemini AI视觉分析与Telegram警报监控X平台品牌提及
If
Set
Code
+13
24 节点Atta
Miscellaneous
使用 NanoBanana、Kling 和 LATE Publishing_Bilsimaging 生成产品品牌动画
使用 Gemini AI、Kling 和 LATE 将产品照片转换为社交媒体视频
If
Set
Code
+13
83 节点Bilel Aroua
Content Creation
使用Gemini、语音和图像生成构建多模态Telegram AI助手
使用Gemini、语音和图像生成构建多模态Telegram AI助手
If
Set
Code
+19
95 节点Iniyavan JC
Personal Productivity
宠物美容发布与预约自动化
使用AI、Facebook和Telegram机器人自动化宠物美容发布与预约
If
Set
Switch
+17
36 节点Christian Moises
AI Chatbot
每日 WhatsApp 群组智能分析:GPT-4.1 分析与语音消息转录
每日 WhatsApp 群组智能分析:GPT-4.1 分析与语音消息转录
If
Set
Code
+20
52 节点Daniel Lianes
Miscellaneous
工作流信息
难度等级
高级
节点数量59
分类3
节点类型16
作者
Nguyen Thieu Toan
@nguyenthieutoanAn AI Automation consultant and system builder specializing in business workflow optimization with n8n. As the Founder of GenStaff, Toan empowers teams to automate complex processes using no-code/low-code tools and AI Agents, making operations smarter and more efficient. He actively shares expertise and tutorials about n8n, AI, and automation on his blog and social channels.
外部链接
在 n8n.io 上查看 →
分享此工作流