资讯动态

inkrss:用JavaScript实现RSS订阅与多通道通知转发

发布时间:2026/10/15 13:06:05 来源:尧图企业网站定制
简介INK RSS 是一套面向 JavaScript 开发者与自建服务爱好者的开源 RSS 订阅与通知方案解决跨平台订阅分散、通知不及时的问题。它基于 Cloudflare Workers 部署前后端与机器人无需自备服务器和数据库适合希望低成本搭建个人 RSS 服务的初中级开发者。资源包为 zip 格式压缩包约 10.53MB文件总数与类型明细上游暂未提供从项目结构看主要包含 Worker 脚本、前端页面与配置说明等 JavaScript 相关文件。已有 150 人浏览学习。功能上订阅端支持网页与 Telegram Bot通知端覆盖 Telegram含 instant view、Bark 与微信最短一分钟即可完成一次监测兼顾即时性与省心。借助现有接口用户可在任意位置订阅、以任意方式接收通知全部服务开源免费便于二次定制与快速部署是搭建个人信息聚合管道的实用参考。1. 从任何地方订阅 RSSinkrss 到底解决了谁的痛点RSS 这东西老网民都不陌生但真正让人放弃它的从来不是「没有好内容」而是「订阅源一多通知就散架」。你在 A 站追一个技术博客在 B 站追一个更新公告在 C 站追一个播客每个平台都要求你装它的 App、开它的推送最后手机通知栏变成垃圾场。inkrss 这个项目就是冲着这个场景来的它用 JavaScript 写了一套 RSS 订阅与通知转发服务核心思路是「订阅归订阅通知归通知」——你把 RSS 源丢给它它负责抓取、解析、去重再通过你指定的通道把新条目推给你。适合谁适合手里有一堆 RSS 源、又不想被某个阅读器绑架的人适合想自己掌控通知链路、甚至想把 RSS 接到自建服务里的开发者。它不解决「内容从哪来」它解决的是「内容怎么到你手里」这一段。2. inkrss 的抓取与解析链路从 feed 地址到结构化条目2.1 为什么选 JavaScript 做 RSS 聚合层RSS 聚合这件事用 Python 能做用 Go 也能做inkrss 选 JavaScript 不是随大流而是因为它的主战场在「通知转发」——而通知转发绕不开 HTTP 请求、JSON 处理、Webhook 拼装这些恰好是 Node.js 生态最顺手的部分。常见做法是用rss-parser或feedparser这类库把 XML 转成对象再用axios或原生fetch去拉取源地址。选 JavaScript 的另一个现实理由是如果你后续想接 Telegram Bot、钉钉机器人、飞书 Webhook这些平台的 SDK 或示例代码大多以 JavaScript 为主复制过来改改就能跑省掉跨语言拼装的麻烦。但要注意JavaScript 做定时抓取有个天然短板单线程事件循环。如果你用setInterval粗暴地每 5 分钟抓 50 个源遇到某个源响应慢整个循环都会被拖住。inkrss 这类项目通常会把抓取任务拆成独立 Promise配合Promise.allSettled做并发控制避免一个坏源拖垮全局。这一点在源数量超过 20 个之后会非常明显属于「不踩不知道踩了才知道疼」的坑。2.2 抓取流程拆解从 URL 到条目对象一个典型的 RSS 抓取流程分四步拉取 XML、解析成对象、标准化字段、比对去重。下面这段代码是我按 inkrss 的常见实现思路写的你可以直接拿去改const Parser require(rss-parser); const crypto require(crypto); const parser new Parser({ timeout: 10000, // 单源超时防止某个源卡死整个任务 headers: { User-Agent: inkrss/1.0 } // 有些源会拦截默认 UA }); // 用 guid 或 link 生成稳定指纹用于去重 function makeFingerprint(item) { const raw item.guid || item.link || item.title; return crypto.createHash(md5).update(raw).digest(hex); } async function fetchFeed(feedUrl) { const feed await parser.parseURL(feedUrl); return feed.items.map(item ({ title: item.title, link: item.link, pubDate: item.pubDate ? new Date(item.pubDate) : new Date(), content: item.contentSnippet || item.content || , fingerprint: makeFingerprint(item) })); }逻辑说明parser.parseURL内部会发 HTTP 请求并解析 XMLtimeout参数必须设否则遇到不响应的源会一直挂起。makeFingerprint是去重的关键——RSS 条目里guid最稳定没有guid就退到link再没有才用title。参数方面User-Agent建议改成你自己的标识部分源会拒绝空 UA 或默认 UA。pubDate要做空值兜底因为不少源不填这个字段直接new Date(undefined)会得到 Invalid Date后续排序会乱。2.3 去重与状态存储别让同一条推两遍去重这件事新手最容易翻车的地方是「只比对标题」。标题会改、会带前后空格、会加「[更新]」前缀只比标题必然重复推送。正确做法是用指纹比对并且把已推送的指纹持久化。inkrss 这类项目一般用轻量存储比如 SQLite 或 JSON 文件。下面是一个基于 SQLite 的去重示例const Database require(better-sqlite3); const db new Database(inkrss.db); db.exec( CREATE TABLE IF NOT EXISTS seen ( fingerprint TEXT PRIMARY KEY, pushed_at INTEGER ) ); function isNew(fingerprint) { const row db.prepare(SELECT 1 FROM seen WHERE fingerprint ?).get(fingerprint); return !row; } function markSeen(fingerprint) { db.prepare(INSERT OR IGNORE INTO seen (fingerprint, pushed_at) VALUES (?, ?)) .run(fingerprint, Date.now()); }参数说明fingerprint做主键天然去重pushed_at记录推送时间方便后续做「只推最近 24 小时」的过滤。用better-sqlite3而不是sqlite3是因为前者是同步 API在定时任务里写起来更直观不用处理回调嵌套。注意数据库文件要放在持久化目录别放在临时目录否则重启后去重记录全丢用户会被重复推送轰炸——这是血泪经验。3. 通知通道配置把新条目推到你想推的地方3.1 Webhook 拼装通用通知模板inkrss 的核心价值在「从任何地方接收通知」所以通知通道必须可配置。最常见的做法是抽象一个notifier接口每个通道实现自己的send方法。下面是一个通用 Webhook 推送函数const axios require(axios); async function sendWebhook(webhookUrl, item, template) { const payload template ? template(item) : { msgtype: text, text: { content: ${item.title}\n${item.link} } }; const res await axios.post(webhookUrl, payload, { timeout: 8000, headers: { Content-Type: application/json } }); if (res.status ! 200) { throw new Error(Webhook 返回非 200: ${res.status}); } }逻辑说明template参数允许你按通道定制消息体比如钉钉要msgtype飞书要msg_typeTelegram 要chat_id和text。timeout设 8 秒超过就放弃避免通知环节拖垮抓取任务。res.status ! 200的判断要保留有些 Webhook 返回 200 但 body 里带错误码严格场景下还要解析 body 判断errcode。3.2 多通道分发与失败重试实际用起来你往往想同时推多个地方一个推手机一个推群组一个存档。这时候要做的是「分发 独立失败」。下面这段代码演示了如何并发推多个通道并且单个通道失败不影响其他通道async function dispatch(item, channels) { const tasks channels.map(async (ch) { try { await sendWebhook(ch.url, item, ch.template); return { channel: ch.name, ok: true }; } catch (err) { return { channel: ch.name, ok: false, error: err.message }; } }); const results await Promise.all(tasks); const failed results.filter(r !r.ok); if (failed.length) { console.warn(部分通道推送失败:, failed); } return results; }参数说明channels是一个数组每项包含name、url、template。用Promise.all而不是allSettled是因为每个任务内部已经 try/catch 了不会抛出。失败通道只记录不重试重试逻辑建议放到外层定时任务里按「失败次数 退避时间」处理别在单次推送里死循环重试否则会把 Webhook 服务打挂。3.3 定时调度cron 表达式与抓取频率抓取频率是个需要权衡的参数。设太短源站压力大容易被封 IP设太长时效性差。常见做法是 15 到 30 分钟一轮。用node-cron配置const cron require(node-cron); // 每 20 分钟执行一次避开整点高峰 cron.schedule(*/20 * * * *, async () { console.log(开始抓取轮次:, new Date().toISOString()); for (const feed of feeds) { try { const items await fetchFeed(feed.url); const newItems items.filter(i isNew(i.fingerprint)); for (const item of newItems) { await dispatch(item, feed.channels); markSeen(item.fingerprint); } } catch (err) { console.error(源 ${feed.url} 抓取失败:, err.message); } } });参数说明*/20 * * * *是标准 cron 表达式表示每 20 分钟。for...of串行处理源是为了避免并发过高被源站限流如果你源不多且想快可以改成Promise.allSettled并发。markSeen放在dispatch之后确保推送成功才标记已读否则推送失败但标记了已读这条就永远丢了。4. 避坑与排查inkrss 落地时最容易翻车的五件事4.1 现象部分源抓取一直超时日志里全是 timeout原因目标源响应慢或者你的出口 IP 被对方限流。RSS 源站很多是个人博客服务器性能有限并发一高就拒绝响应。解决给每个源单独设timeout并且把慢源标记出来降低它的抓取频率。可以在配置里加一个interval字段慢源单独用更长的间隔。另外User-Agent一定要设成可识别的别用空 UA很多源会直接拒绝。4.2 现象同一条内容被推了三四遍原因去重指纹不稳定。有的源每次请求guid都会变有的源link带随机参数导致指纹每次都不同。解决优先用guid没有guid时用link去掉查询参数后的部分再没有才用title pubDate组合。如果源本身就不稳定可以在配置里对该源关闭去重改用「只推最近 N 小时」的时间窗口过滤。4.3 现象Webhook 返回 200 但消息没收到原因不同平台的 Webhook 对消息体格式要求不同返回 200 只代表请求收到了不代表消息格式正确。比如钉钉要求msgtype字段飞书要求msg_type字段名写错就是静默失败。解决每个通道单独写模板并且把 Webhook 返回的 body 打印出来。钉钉的 body 里会有errcode飞书会有code非 0 就是格式问题。别只看 HTTP 状态码。4.4 现象服务跑几天后内存暴涨原因抓取任务里累积了大量未释放的对象或者数据库连接没关。Node.js 虽然自动 GC但如果你把每次抓取的条目都塞进一个全局数组做「历史记录」内存迟早爆。解决历史记录只存指纹和时间戳不存完整内容。数据库连接用单例别每次抓取都新建。定时任务里避免闭包引用大对象抓取完及时把items置空。4.5 现象cron 任务重叠执行同一轮抓取跑了两遍原因上一轮还没跑完下一轮定时又触发了。抓取 50 个源如果每个都慢一轮可能超过 20 分钟。解决加一个运行锁用布尔变量或文件锁都行。任务开始时检查锁如果上一轮还在跑直接跳过本轮并打日志。别用setInterval用 cron 配合锁更可控。5. 进阶玩法把 inkrss 接到自建服务与内容归档5.1 用 Webhook 做内容归档通知只是第一步很多人还想把 RSS 内容存档。做法很简单在dispatch里加一个「归档通道」把条目写进数据库或 Markdown 文件。下面是一个写 Markdown 的示例const fs require(fs); const path require(path); function archiveToMarkdown(item, feedTitle) { const date item.pubDate.toISOString().slice(0, 10); const dir path.join(__dirname, archive, feedTitle); fs.mkdirSync(dir, { recursive: true }); const filename ${date}-${item.fingerprint.slice(0, 8)}.md; const content # ${item.title}\n\n 来源: ${item.link}\n 时间: ${item.pubDate.toISOString()}\n\n${item.content}\n; fs.writeFileSync(path.join(dir, filename), content, utf8); }参数说明feedTitle用来分目录避免所有源混在一起。fingerprint.slice(0, 8)做文件名后缀防止同一天多条内容重名。recursive: true保证目录不存在时自动创建。这个归档函数可以挂在dispatch之后作为「静默通道」执行不参与通知失败判断。5.2 验证抓取是否正常三个检查点部署完 inkrss 后别急着等通知。先手动跑一轮检查三件事第一fetchFeed返回的条目数是否和源站显示的一致第二isNew对同一条内容连续调用两次第二次必须返回 false第三sendWebhook的返回 body 里错误码是否为 0。这三个检查点过了基本就不会有大问题。我一般会写一个debug.js单独调用这几个函数把中间结果打印出来。比看日志快得多尤其是排查指纹问题时直接把两条内容的指纹打出来对比一眼就能看出是不是guid在变。5.3 一个具体技巧用环境变量管理多套配置inkrss 这类服务通常要跑在服务器上配置别硬编码。用dotenv把 Webhook 地址、数据库路径、抓取间隔都放到.env里# .env FEED_INTERVAL*/20 * * * * DB_PATH./data/inkrss.db WEBHOOK_PHONEhttps://example.com/hook/phone WEBHOOK_GROUPhttps://example.com/hook/group然后在代码里用process.env.WEBHOOK_PHONE读取。这样做的好处是换环境不用改代码本地测试和线上部署用同一套逻辑。注意.env别提交到仓库加进.gitignore。从那以后我每次部署这类抓取服务都强制先跑一轮手动抓取把指纹和 Webhook 返回 body 打出来看一遍确认没问题再交给 cron。这个习惯帮我省掉了至少三次「半夜被重复推送吵醒」的尴尬。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑