Karakeep(Hoarder)RSS 集成完全指南:将列表发布为订阅源,并自动抓取外部 Feed 生成书签
【免费下载链接】hoarderA self-hostable bookmark-everything app (links, notes and images) with AI-based automatic tagging and full text search项目地址: https://gitcode.com/GitHub_Trending/ho/hoarder
本篇技术指南聚焦开源项目 Karakeep(原 Hoarder)的 RSS 集成能力,涵盖两大方向:一是把任意列表(List)发布为带访问令牌保护的 RSS 订阅源,供 RSS 阅读器或他人订阅;二是监控外部 RSS/Atom 源,每小时自动抓取新条目并生成书签、可选导入分类标签。读完本文,你将掌握完整的界面操作步骤、URL 与令牌机制、字段映射规则,以及背后的 Worker 调度、去重、配额检查等源码级实现原理。
RSS 集成总览:发布与消费的双向能力
Karakeep 的 RSS 集成是双向的,对应两条独立的技术链路:
- 发布(Publishing):将你创建的任意列表发布为 RSS Feed,其他人或 RSS 阅读器可以通过一个带 token 的 URL 订阅你的书签集合;
- 消费(Consuming):在设置中添加外部 RSS/Atom 源,后台 Worker 会定时抓取,把新条目自动保存为书签,适合跟踪博客、新闻站点等内容源。
这两条链路在代码中分属不同的模块:发布侧位于 API 层的 rss.ts 与 utils/rss.ts,消费侧则由独立的 Feed Worker 驱动(feedWorker.ts)。
将列表发布为 RSS 订阅源
启用 RSS 的界面操作
按官方文档的步骤,进入任意列表后:
- 导航到你的某个列表;
- 点击列表设置(三点菜单);
- 打开RSS Feed开关;
- 复制生成的 RSS Feed URL。
从源码看,开关背后对应列表在数据库中的rssToken字段。在 lists.ts 中,token 的生成与读取由List.getOrCreateRSSKey/getRSSKey实现,每次重新生成会写入新的 token 值,从而让旧 URL 立即失效。
Feed URL 的结构与参数
生成的 Feed URL 形如:
https://<你的实例地址>/v1/rss/lists/<listId>?token=<访问令牌>发布端点定义在 rss.ts,路由为GET /lists/:listId,支持两个查询参数:
| 参数 | 类型 | 说明 |
|---|---|---|
token | string(可选) | 访问令牌。列表未公开时必须提供,且必须与数据库中的rssToken完全一致 |
limit | number(可选) | 返回条数,范围1到MAX_NUM_BOOKMARKS_PER_PAGE,默认20,按创建时间倒序(order: "desc")返回 |
响应头为Content-Type: application/rss+xml,返回标准的 RSS 2.0 XML。Feed 的标题会自动带上列表图标与名称,例如Bookmarks from 📚 我的收藏;feedUrl指向 API 端点,siteUrl指向 Web 端的列表页面(/dashboard/lists/<listId>)。
RSS 中发布的内容与字段映射
文档明确了 Feed 中包含的内容类型,其具体映射逻辑在 utils/rss.ts 中实现:
| 书签内容 | 是否包含 | RSS 字段映射 |
|---|---|---|
| 链接(Link) | ✅ | url取书签 URL,author取页面作者,description取书签描述,title取书签标题 |
| 资产(Asset) | ✅ | url指向资产的查看地址,由publicUrl + getAssetUrl(assetId)拼接,用于查看 PDF、图片等上传文件 |
| 标签(Tags) | ✅ | 作为 RSS 的<category>分类元素导出 |
| 日期 | ✅ | 书签创建时间createdAt作为<pubDate>发布日期 |
| 纯文本笔记(Note) | ❌ | 不包含——笔记没有关联 URL,无法作为链接条目导出 |
每个条目的<guid>使用书签 ID,确保条目在阅读器中的唯一性与稳定更新。同时注意一个安全细节:在生成条目之前,代码会过滤掉javascript:、data:等不安全协议的历史链接(通过isAllowedBookmarkUrl校验),防止阅读器跟踪恶意 URL。
安全模型:令牌、重新生成与即时失效
- 唯一令牌访问:每个 Feed 需要唯一的 token 才能访问,对应数据库中的
rssToken字段; - 随时重新生成:重新生成 token 会覆盖旧值,旧 URL 立即失效;
- 关闭即撤销:禁用列表的 RSS 开关后,
rssToken不再有效,访问即被拒绝。
在 lists.ts 的getPublicList查询中可以印证访问控制逻辑:列表可被访问的条件是「列表本身标记为公开(public = true)」或「传入的 token 与rssToken相等」。也就是说,只要列表启用了公开访问或持有有效 token,就能读取内容。
订阅并自动抓取外部 RSS 源
添加 Feed 的界面步骤与字段
文档中的操作路径为设置(Settings)→ RSS Feeds → Add Feed,需要填写的字段如下:
- Name:Feed 的友好名称;
- URL:RSS/Atom 源的地址;
- Enabled:是否启用该 Feed;
- Import Tags:是否把 RSS 分类(categories)作为标签附加到生成的书签上。
这些字段与 feeds.ts 中的 zod 校验约束一一对应,底层约束值得注意:
| 字段 | 类型 | 校验约束 |
|---|---|---|
name | string | 必填,长度1–100 |
url | string | 必填,必须是合法 URL,最长2000字符 |
enabled | boolean | 是否启用 |
importTags | boolean | 默认false,仅新建时可设置 |
每小时自动检查的调度机制
文档说明 Karakeep每隔一小时检查启用的 Feed。源码中的实现比「整点扫描」更精细:在 feedWorker.ts 中,一个 cron 表达式为0 * * * *的调度器在每小时的第 0 分钟运行,它先查询所有enabled = true的 Feed,然后通过一个基于 Feed ID 哈希的getFeedMinuteOffset函数,把每个 Feed 映射到当前小时的某个分钟偏移(0–59),再以对应延迟将抓取任务入队。
这样做的好处是把大量 Feed 的抓取均匀分散在一小时内的各个时间点,避免整点并发风暴。每个任务还带有一个idempotencyKey(格式为<feedId>-<小时窗口>),保证同一小时内同一 Feed 不会被重复调度。
新条目去重与书签创建流程
在 feedWorker.ts 的run函数中,抓取流程包含完整的状态机:
- 配额预检:通过
QuotaService.canCreateBookmark检查用户是否还有书签配额,配额不足时直接跳过本轮抓取(返回success,不算失败); - 网络抓取:使用
fetchWithProxy请求 Feed URL,携带 5 秒超时、User-Agent: Karakeep-RSS/1.0与 XML 相关的 Accept 头; - 响应校验:HTTP 状态必须为 200,且
Content-Type必须包含xml,否则记录failure并等待下一轮; - 解析条目:通过
parseFeedItems(基于rss-parser,feedParser.ts)把 XML 解析为条目列表,每个条目的guid按guid ?? id ?? link的优先级推导; - 去重:查询
rssFeedImportsTable中该 Feed 已导入的entryId,只保留既无guid重复、又具备link与guid的新条目; - 批量创建:通过模拟用户身份的 tRPC 客户端(
buildImpersonatingTRPCClient)调用bookmarks.createBookmark,类型固定为 LINK,source标记为"rss",便于溯源; - 标签导入:若启用了
importTags,且条目带有 categories,则通过bookmarks.updateTags把每个分类作为标签附加到对应书签; - 记录导入映射:把
entryId ↔ bookmarkId的映射写入rssFeedImportsTable(onConflictDoNothing幂等),作为后续去重的依据。
抓取完成后,Feed 记录会更新lastSuccessfulFetchAt时间戳,任务结果(success/failure)会写入lastFetchedStatus与lastFetchedAt——这正是设置页面中展示 Feed 健康状态的字段来源。
手动立即抓取
除了每小时自动调度,还可以手动触发某个 Feed 立即抓取。REST 端点POST /feeds/:feedId/fetch(feeds.ts)以及 tRPC 的feeds.fetchNow(routers/feeds.ts)都会把该 Feed 的抓取任务直接投入队列,无需等待下一个小时窗口。
面向开发者的 API 与数据模型一览
REST 端点(Hono 实现)
发布侧与消费侧的管理接口均定义在 packages/api/routes 下:
| 方法 | 路径 | 说明 |
|---|---|---|
GET | /v1/rss/lists/:listId | 获取列表的 RSS Feed(公开端点,无需登录,靠 token 鉴权) |
GET | /feeds | 列出当前用户的全部 Feed |
POST | /feeds | 新建 Feed |
GET | /feeds/:feedId | 获取单个 Feed 详情 |
PATCH | /feeds/:feedId | 更新 Feed(名称、URL、启用状态等) |
DELETE | /feeds/:feedId | 删除 Feed |
POST | /feeds/:feedId/fetch | 手动触发抓取 |
tRPC 路由器与所有权隔离
管理类操作同样以 tRPC 形式暴露:feedsAppRouter(routers/feeds.ts)提供create / update / get / list / delete / fetchNow六个过程,统一走createScopedAuthedProcedure("feeds")鉴权。其中update / get / delete / fetchNow通过ensureFeedOwnership中间件先加载 Feed 并校验归属,越权访问会抛出User is not allowed to access resource错误。
数据模型
- rssFeedsTable:存储用户订阅的外部 Feed,包含
id、userId、name、url、enabled、importTags、lastFetchedStatus、lastFetchedAt、lastSuccessfulFetchAt等字段(见 schema.ts 中rssFeedsTable定义); - rssFeedImportsTable:记录每个 Feed 已导入的条目(
entryId)与生成书签(bookmarkId)的映射,是去重机制的核心。
测试覆盖与边界限制
feeds.test.ts 提供了完整的路由级测试,可以佐证以下行为:
- 创建/更新/列表/删除的常规 CRUD 均通过测试用例覆盖;
- Feed 数量上限为 1000 个:创建第 1001 个 Feed 会抛出
Maximum number of RSS feeds (1000) reached; - 跨用户隔离:用户 A 无法删除或更新用户 B 的 Feed,列表接口也只返回自己的 Feed;
- 删除不存在的 Feed 或所有权检查后行消失,会返回
Feed not found/NOT_FOUND。
适用场景与注意事项
推荐场景:把公开列表转成订阅源分享给团队或读者;把博客、技术媒体、更新公告等 RSS/Atom 源接入 Karakeep,实现自动收藏归档。
注意事项:
- RSS 发布依赖
rssToken,分享 URL 时务必完整携带?token=...参数,token 泄露后应立即在列表设置中重新生成; - 纯文本笔记不会出现在发布的 RSS 中,如需对外分享笔记,请使用列表的公开分享链接;
- 抓取频率以小时为单位,且分散在一小时内的不同分钟,对时效性要求极高的场景需手动触发
fetch; - 抓取器要求响应
Content-Type含xml,某些非标准实现的源可能因响应头问题被判定为失败; - Feed 生成的书签会占用用户书签配额,配额不足时抓取会被跳过。
掌握以上双向链路后,你既可以把 Karakeep 当作一个「个人书签的 RSS 输出站」,也可以把它变成「外部内容的自动采集器」,两种能力都可在 [设置 → RSS Feeds] 与列表设置中零代码完成。
【免费下载链接】hoarderA self-hostable bookmark-everything app (links, notes and images) with AI-based automatic tagging and full text search项目地址: https://gitcode.com/GitHub_Trending/ho/hoarder
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考