cflow是我在开源项目memos和cflow的基础上二次修改开发的一款笔记工具,相关介绍: https://dub.sh/sscflow
经过我昨天的折腾,实现了telegram发送数据到n8n的同步。
一、Docker部属n8n
我直接使用的绿联nas拉取的镜像并部署。
部属n8n的教程网上很多,关键词:Docker n8n 部属 能找到一堆
例如, https://n8n.akashio.com/article/n8n-deployment-method
强调几个点:
- 挂载路径映射的文件是/home/node/.n8n
- 我添加了一个环境变量WEBHOOK_URL,这个主要是后面telegram触发器会用到,如果不使用https会报错。由于我是部署在本地nas里所以我使用的是cloudflare tunnel进行的内网穿透。
- 本地部属完n8n想要访问,需要反向代理,我使用的是Lucky
二、telegram单向同步cflow
2.1 添加一个Telegram Trigger
- Trigger On 我选择的是*
2. Telegram account这里我填写的Access Token为Telegram bot token ,Base URL我没有使用默认的URL会链接失败...
api.telegram.org是可以用cloudflare代理的,创建一个workers,代码是:

const tg_host = "api.telegram.org"; addEventListener('fetch', event => { event.respondWith(handleRequest(event.request)) }) async function handleRequest(request) { var u = new URL(request.url); u.host = tg_host; var req = new Request(u, {method: request.method,headers: request.headers,body: request.body}); const result = await fetch(req); return result; }
然后绑定你的域名,这个地址替换为Base URL即可
- 给机器人发一个消息,或者把机器人加入频道(并给管理员权限)在频道里发送消息后,点击Test step就可以获取数据了
2.2 Telegram内容格式转换
添加一个code节点,JavaScript内容如下,目的是将Telegram的内容转换为标准的Markdown格式
// 处理链接 function extractLinks(text, entities) { if (!text || !entities) return text || ''; let result = text; let links = entities.filter(e => e.type === 'text_link').sort((a, b) => b.offset - a.offset); for (const link of links) { const part = result.substring(link.offset, link.offset + link.length); result = result.substring(0, link.offset) + `[${part}](${link.url})` + result.substring(link.offset + link.length); } return result; } // 获取消息直链 function generateMessageLink(channelPost) { if (!channelPost || !channelPost.chat || !channelPost.chat.id || !channelPost.message_id) { return null; } const chatId = channelPost.chat.id; let chatIdForUrl = chatId.toString(); // 处理各种可能的频道ID格式 if (chatIdForUrl.startsWith('-100')) { chatIdForUrl = chatIdForUrl.substring(4); // 去掉'-100' } return `https://t.me/c/${chatIdForUrl}/${channelPost.message_id}`; } // 从前一个Code节点获取消息链接 let messageLink; try { messageLink = $('GetMessageLink').item[0].message_link; } catch (error) { // 如果无法从前一个节点获取,则尝试自己生成 messageLink = generateMessageLink($json.channel_post); } // 获取发送源信息 const channelPost = $json.channel_post; const sourceTitle = channelPost?.forward_from_chat?.title || channelPost?.sender_chat?.title || '未知来源'; // 获取消息内容(支持text和caption两种情况) const messageText = channelPost?.text || channelPost?.caption || ''; const messageEntities = channelPost?.entities || channelPost?.caption_entities || []; // 转换为Markdown格式 const formattedText = extractLinks(messageText, messageEntities); // 判断是否包含图片 const hasPhoto = channelPost?.photo && channelPost.photo.length > 0; // 创建一个格式化上海时区的函数 function formatShanghaiTime(timestamp) { // 创建一个日期对象 const date = new Date(timestamp * 1000); // 格式化为上海时区的时间 return new Intl.DateTimeFormat('zh-CN', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).format(date); } // 日期格式化(使用上海时区) const messageDate = channelPost?.date ? formatShanghaiTime(channelPost.date) : formatShanghaiTime(Math.floor(Date.now() / 1000)); // 获取media_group_id const mediaGroupId = channelPost?.media_group_id || ''; // 生成内容 let content = `来自Telegram的消息\n`; // 添加消息内容 if (formattedText && formattedText.trim() !== '') { content += `**内容:**\n${formattedText}\n`; } // 添加消息来源和时间 content += `**时间:** ${messageDate}\n`; content += `**来源:** [${sourceTitle}](${messageLink})`; // 添加标签 content += ` #telegram`; // 如果有图片,添加图片标记 if (hasPhoto) { content += ` #包含图片`; } // 如果是转发的消息,添加转发标记 if (channelPost?.forward_origin || channelPost?.forward_from_chat) { content += ` #转发`; } // 添加一个隐藏的media_group_id标记(用于后续识别同一相册的消息) if (mediaGroupId) { content += `\n<!-- telegram_media_group_id:${mediaGroupId} -->`; } return { content: content, visibility: "PRIVATE", mediaGroupId: mediaGroupId // 同时返回media_group_id,方便后续节点使用 };
2.3 创建cflow卡片
- Method选POST
- Authentication选Generic Credential Type
- Generic Auth Type选择Header Auth
- 点击Header Auth,Create new credential
填写cflow获取的token
6. 开启Send Body,Body Content Type选择JSON,Specify Body选择Using Fields Below,Name填写content,Value填写{{ $json.content }}

如果Telegram的内容只是简单的文本,到这一步已经结束了。

2.4 获取cflow卡片ID
目的是Telegram如果有图片需要将图片上传到cflow后关联对应的卡片id
code节点的代码是:
// 从创建memo的响应中提取ID const response = $json; let memoId = null; if (response && response.id) { memoId = response.id; } return { memo_id: memoId };
2.5 获取图片路径
// 引用Telegram Trigger节点的数据 const telegramData = $node["Telegram Trigger"].json; const photos = telegramData.channel_post?.photo || []; // 按尺寸降序排序,获取最高质量的图片 const bestPhoto = photos.sort((a, b) => b.width * b.height - a.width * a.height)[0]; if (bestPhoto && bestPhoto.file_id) { // 准备获取文件URL的参数 return { file_id: bestPhoto.file_id }; } else { // 没有图片,返回null return { file_id: null }; }
2.6 获取文件信息

- URL:{cloudflare代理tgurl}/bot{tgbot token}/getFile
- Send Query Parameters开启,Name填写file_id,Value为{{ $json.file_id }}
2.7 构建图片URL
// 处理Telegram API返回的文件信息 const response = $json; if (response.ok && response.result && response.result.file_path) { // 构建文件下载URL - 将YOUR_BOT_TOKEN替换为你的实际Token const fileUrl = `https://tg.brmys.cn/file/bot6361745326:AAEQeSNkNzCN3kJn6lpvzR_9h4CqXh1uZjQ/${response.result.file_path}`; return { file_url: fileUrl }; } else { return { file_url: null }; }
2.8 下载图片
下载为二进制数据
- Method为Get
- URL为{{ $json.file_url }}
- Options选择为Response,Response Format选择为File,Put Output in Field填写data

2.9 图片格式处理
这一步的目的是讲图片格式改为image/jpeg,不然cflow无法识别图片
// 获取当前项的二进制数据 const binaryData = $input.item.binary.data; // 获取文件名分析扩展名 const fileName = $input.item.json.file_path || $input.item.json.file_url || 'file_0.jpg'; const ext = fileName.split('.').pop().toLowerCase(); // 根据扩展名设置正确的MIME类型 let mimeType = 'image/jpeg'; // 默认为JPEG if (ext === 'png') { mimeType = 'image/png'; } else if (ext === 'gif') { mimeType = 'image/gif'; } else if (ext === 'webp') { mimeType = 'image/webp'; } // 返回修改后的数据,保持二进制数据不变,只修改MIME类型 return { json: { ...$input.item.json, mimetype: mimeType, mimeType: mimeType }, binary: { data: { ...$input.item.binary.data, mimeType: mimeType } } };
2.10 上传图片到cflow

2.11获取资源ID
code节点的代码是:
// 从上传响应中提取资源ID const response = $json; let resourceId = null; // 处理不同的响应格式 if (response.data && response.data.id) { resourceId = response.data.id; } else if (response.id) { resourceId = response.id; } return { resource_id: resourceId };
2.12 关联卡片资源到cflow
- Method选择PATCH
- URL为:http://192.168.10.2:5236/api/v1/memo/{{ $('获取cflow卡片ID').item.json.memo_id }}
- JSON为
{ "resourceIdList": [{{$node["上传图片到cflow"].json.id}}] }
