深入解析 Karakeep(原 Hoarder)系统架构:Next.js 前端、SQLite 任务队列与三类 Worker 流水线
【免费下载链接】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)是一个可自托管的"收藏一切"应用,支持收藏链接、笔记与图片,并提供基于 AI 的自动打标签和全文搜索。本文以 v0.28.0 时期的架构文档(docs/versioned_docs/version-v0.28.0/07-development/04-architecture.md)为骨架,结合当前仓库源码逐层拆解其整体架构:从 Next.js Web 应用、SQLite 数据存储与任务队列,到 Crawling(爬取)、OpenAI(AI 推理)、Indexing(全文索引)三类核心 Worker,并顺带梳理其随版本演进出的更多 Worker 类型。读完本文,你将理解一条收藏链接从保存到可被全文搜索的完整调用链,以及如何通过环境变量与 Docker Compose 部署这套架构。
架构总览:三个核心组件
v0.28.0 的架构文档用极为精炼的方式给出了系统的全局视图,整个系统由三个核心组件构成:
- Web 应用(Webapp):基于 Next.js,使用 SQLite 存储数据;
- Worker 集群(Workers):从基于 SQLite 的任务队列中消费任务并执行;
- 辅助基础设施:无头 Chrome 浏览器(用于抓取页面)、Meilisearch(用于全文索引)。
其中 Worker 在 v0.28.0 文档中定义了三种任务类型:
| 任务类型 | 职责 | 关键依赖 |
|---|---|---|
| Crawling(爬取) | 使用运行在 Worker 容器内的无头 Chrome 浏览器抓取链接内容 | Headless Chrome |
| OpenAI(AI 推理) | 调用 OpenAI API 对内容进行标签推断(自动打标签) | OpenAI API |
| Indexing(索引) | 将内容索引到 Meilisearch,以加速搜索时的检索 | Meilisearch |
这张图揭示了一个重要的设计取向:用户请求处理(Web 应用)与重型后台处理(爬取、AI 推理、索引)完全解耦。用户在界面上保存一条链接后,Web 应用只负责写入元数据并投递一个任务到队列,真正的"重活"全部由 Worker 异步完成,因此 Web 应用的响应延迟不会受爬取或 AI 调用耗时的影响。
Web 应用层:Next.js + SQLite 的组合
Web 应用位于 apps/web,是一个基于 Next.js 的完整前端应用(App Router 结构),承载登录注册、仪表盘、书签管理、设置、阅读器等页面。其数据层使用 SQLite,通过 packages/db/schema.ts 中基于 Drizzle ORM 定义的 schema 来建表和读写。
从 schema.ts 可以窥见核心数据模型,包括:
- 用户(user):
users表,包含账号、角色(admin/user)、书签配额、存储配额,以及自动打标签、自动摘要、标签风格(如lowercase-hyphens、camelCase)等 AI 相关设置; - 书签(bookmark):链接、笔记、图片三类收藏的统一实体,通过
BookmarkTypes区分; - 任务状态字段:书签行上直接带有
taggingStatus、summarizationStatus、embeddingStatus、crawlStatus等状态列,这些状态正是 Worker 流水线各阶段的"进度仪表盘"。
选择 SQLite 意味着单文件、零运维,这也是该项目"自托管友好"定位的关键支撑——用户无需额外搭建 PostgreSQL,只需挂载一个数据目录即可完成部署(见下文 Docker Compose 部分)。
任务队列:以 SQLite 为存储的轻量级队列
架构文档强调"Workers 消费来自基于 SQLite 的任务队列中的任务"。在当前仓库中,这套队列机制被抽象为一组与存储后端解耦的接口,定义在 packages/shared/queueing.ts:
Queue<T>:队列的抽象接口,提供enqueue(payload, options)投递任务、stats()查询 pending/running/failed 等队列状态,支持idempotencyKey(幂等键)、priority(优先级)、delayMs(延迟执行)、groupId等投递选项;Runner<T>:Worker 端的运行器抽象,run()启动消费循环,通过pollIntervalMs(轮询间隔)、timeoutSecs(任务超时)、concurrency(并发数)等参数控制消费行为;QueueRetryAfterError:一种特殊错误,抛出后任务会不消耗重试次数地延迟重试——源码注释明确说明这是为限流(rate limiting)场景设计的(见 queueing.ts)。
队列的具体存储实现通过插件机制注册。默认的 SQLite 队列实现位于 packages/plugins/queue-liteque,它通过 PluginManager 以PluginType.Queue类型自动注册为 "Liteque" 提供者(见 queue-liteque/index.ts)。这也意味着架构文档所描述的"SQLite 队列"是可以被替换的——仓库中还存在基于 Restate 的queue-restate插件,供需要分布式队列能力的部署场景使用。
Worker 集群:三类核心任务的内部实现
1. Crawling:无头 Chrome 抓取链接内容
爬取任务的入口是 apps/workers/workers/crawlerWorker.ts,其核心执行函数runCrawler(crawlerWorker.ts#L331)展示了完整的爬取流水线:
- 任务校验:用
zCrawlLinkRequestSchema校验任务载荷,非法任务直接丢弃; - 域名限流检查:
checkDomainRateLimit按目标域名维度做限流(窗口期/最大请求数由CRAWLER_DOMAIN_RATE_LIMIT_WINDOW_MS等配置控制),被限流时抛出QueueRetryAfterError并加入 40% 随机抖动(jitter)避免"惊群效应"(见 crawlerWorker.ts#L209-L257); - 内容类型探测:通过
getContentTypeAndMetadata预探测 URL 的内容类型——若为 PDF 或图片,则把链接书签"就地转换"为对应的资产书签(handleAsAssetBookmark),否则走 HTML 网页路径; - 浏览器抓取与解析:调用 crawler/crawlAndParse.ts 中的
crawlAndParseUrl完成页面抓取、解析与资产持久化。浏览器生命周期管理(连接无头 Chrome、初始化 adblocker 等)封装在 crawler/browser.ts; - 归档收尾:抓取成功后执行截图、PDF、完整页面归档等可选归档逻辑(
archivalLogic); - 投递后续任务:
enqueuePostCrawlJobs(crawlerWorker.ts#L264-L329)把本次抓取的"成果"转交给流水线的下一环——按配置投递 Embeddings/OpenAI 任务(打标签、摘要)、搜索重建索引任务,以及视频下载与 webhook 触发任务。
值得注意的是,Worker 运行器在onComplete中会把书签链接的crawlStatus置为success;在重试耗尽(numRetriesLeft == 0)的onError中则会把crawlStatus置为failure,并在同一个事务里清理taggingStatus、summarizationStatus、embeddingStatus等遗留的 pending 状态(见 crawlerWorker.ts#L125-L192),保证失败任务不会在后续环节"悬空"。
2. OpenAI:AI 自动打标签与内容推断
在 v0.28.0 的架构里,OpenAI 任务负责"推断内容标签"。演进到当前仓库后,这部分已扩展为 apps/workers/workers/inference 目录下的inferenceWorker.ts、tagging.ts与summarize.ts三个文件,即除了自动打标签,还加入了自动摘要能力。用户侧相关的开关(autoTaggingEnabled、autoSummarizationEnabled、标签风格tagStyle、标签语言inferredTagLang等)都定义在用户表的 schema 中(见 packages/db/schema.ts#L100-L117)。
从enqueuePostCrawlJobs的源码可以看出推理任务的触发策略:默认情况下,若开启了自动向量索引(embedding.enableAutoIndexing),抓取完成后会先投递 Embeddings 任务并在向量化完成后触发打标签(runTaggingOnComplete: true);否则直接投递 OpenAI 打标签任务,并总是投递一个摘要任务(见 crawlerWorker.ts#L277-L303)。
3. Indexing:向 Meilisearch 建立全文索引
索引任务由 apps/workers/workers/searchWorker.ts 实现,负责把书签内容写入 Meilisearch 以支持高速全文搜索。搜索引擎同样通过插件机制接入:packages/plugins/search-meilisearch 在检测到 Meilisearch 配置(isConfigured())后,以PluginType.Search注册为 "MeiliSearch" 提供者(见 search-meilisearch/index.ts)。搜索索引的触发方式通过 packages/shared/search.ts 中的triggerSearchReindex统一封装,爬取完成后即调用它入队重建索引任务(见 crawlerWorker.ts#L306)。
从源码结构看架构的演进:远不止三类任务
v0.28.0 文档仅列了三种任务类型,但当前仓库的 Worker 入口 apps/workers/index.ts 已显式定义了 13 种 Worker 构建器(workerBuilders),可以推断架构随版本演进大幅扩展:
- 核心三件套:
crawler(爬取)、inference(AI 推理)、search(搜索索引); - 围绕爬取的配套:
lowPriorityCrawler(低优先级爬取队列)、embeddings(向量化)、video(视频下载)、assetPreprocessing(资产预处理); - 增值能力:
feed(RSS 订阅刷新)、ruleEngine(规则引擎)、webhook(Webhook 触发)、backup(自动备份)、adminMaintenance(后台维护,如整理资产、迁移链接 HTML 内容)、以及轮询式启动的import(批量导入)。
每种 Worker 的启用与禁用均可通过WORKERS_ENABLED/WORKERS_DISABLED环境变量精确控制(见 apps/workers/index.ts#L114-L125),这为自托管用户在资源受限环境下的裁剪部署提供了极大灵活性——例如纯笔记用户完全可以禁用爬取与 AI 相关的 Worker。
部署拓扑:一条 Docker Compose 背后的三个容器
架构文档描述的组件在实际部署中被编排进 docker/docker-compose.yml,共三个服务:
- web:
ghcr.io/karakeep-app/karakeep镜像,映射宿主机3000端口,挂载data:/data数据卷(即 SQLite 数据文件所在目录),并通过环境变量指向另外两个服务:MEILI_ADDR: http://meilisearch:7700与BROWSER_WEB_URL: http://chrome:9222; - chrome:
ghcr.io/karakeep-app/karakeep-chrome镜像,即架构图与文档中提到的"运行在 Worker 容器内的无头 Chrome 浏览器"。它通过--disable-gpu、--disable-dev-shm-usage、--disable-blink-features=AutomationControlled、--window-size=1440,900等参数以无头模式启动,并暴露调试端口 9222 供 Worker 连接(docker-compose.yml#L22-L31); - meilisearch:
getmeili/meilisearch:v1.41.0镜像,挂载meilisearch:/meili_data卷,并设置MEILI_NO_ANALYTICS: "true"关闭遥测(docker-compose.yml#L32-L40)。
这套拓扑正是架构文档"Web 应用 + Worker + 辅助服务"抽象的具体落地:Web 与 Worker 打包在同一个镜像里(通过环境变量决定各进程的行为),Chrome 与 Meilisearch 作为独立容器提供能力。
关键环境变量:从配置源码看可调参数
Worker 侧的运行参数集中在 packages/shared/config.ts 的crawler配置块(config.ts#L398-L437)中,常用项包括:
| 环境变量 | 作用 | 默认说明 |
|---|---|---|
CRAWLER_NUM_WORKERS | 爬取 Worker 并发数 | 影响爬取吞吐,对应 Runner 的concurrency |
CRAWLER_JOB_TIMEOUT_SEC | 单个爬取任务的超时时间 | 对应 Runner 的timeoutSecs |
BROWSER_WEB_URL/BROWSER_WEBSOCKET_URL | 无头 Chrome 的连接地址(HTTP 调试端口或 WebSocket) | Compose 中指向http://chrome:9222 |
BROWSER_CONNECT_ONDEMAND | 是否按需建立浏览器连接 | 影响浏览器资源占用模式 |
CRAWLER_DOMAIN_RATE_LIMIT_WINDOW_MS/CRAWLER_DOMAIN_RATE_LIMIT_MAX_REQUESTS | 按域名限流的窗口与请求上限 | 同时设置才启用,配合QueueRetryAfterError使用 |
CRAWLER_STORE_SCREENSHOT/CRAWLER_FULL_PAGE_SCREENSHOT/CRAWLER_STORE_PDF/CRAWLER_FULL_PAGE_ARCHIVE | 控制截图、PDF、整页归档等归档行为 | 归档功能开关 |
CRAWLER_VIDEO_DOWNLOAD及CRAWLER_VIDEO_DOWNLOAD_MAX_SIZE | 是否下载视频及大小上限 | 控制视频类书签的处理 |
CRAWLER_ENABLE_ADBLOCKER/CRAWLER_ENABLE_AUTOCONSENT | 是否启用广告拦截与 Cookie 同意自动处理 | 影响页面抓取质量与合规 |
完整的环境变量清单见 环境变量配置文档。
一次收藏的完整旅程:端到端链路回顾
综合以上分析,一条链接被保存后经历的整体链路可以概括为:
- 用户在 Web 应用保存链接,书签行落库到 SQLite,同时向任务队列投递 Crawling 任务;
- Crawling Worker 消费任务:先做域名限流检查与内容类型探测,再驱动无头 Chrome 抓取页面、解析正文并归档截图/PDF/整页快照等资产;
- 抓取成功后,
enqueuePostCrawlJobs依次投递 Embeddings/OpenAI 任务(自动打标签、摘要)与搜索重建索引任务; - OpenAI/Embeddings Worker 对内容做 AI 推理,写回标签与摘要;
- Search Worker 把内容写入 Meilisearch;
- 用户在搜索框中输入关键词,Web 应用通过 Meilisearch 快速返回全文检索结果。
整条链路中,SQLite 既是业务数据的主存储,也是任务队列的存储介质;无头 Chrome 承担"重"的页面渲染抓取;Meilisearch 承担"快"的全文检索;OpenAI 类服务承担"智能"的内容理解。三者通过队列解耦、通过状态列追踪进度,构成了一个结构清晰、各司其职、易于自托管的可插拔架构。
小结
本文从 v0.28.0 架构文档的三句话出发,结合当前仓库源码还原了 Karakeep 的完整架构面貌:Next.js + SQLite 的 Web 应用层、可插拔的队列抽象(默认 SQLite 队列)、三类核心 Worker 的流水线实现,以及它们在 Docker Compose 中的部署拓扑。理解这套架构,无论是排查爬取失败、调优 Worker 并发,还是为资源受限环境裁剪 Worker 类型,都能有的放矢。更进一步,仓库文档目录还提供了 开发环境搭建 与 数据库说明 等资料,可作为继续深入该架构的下一站。
【免费下载链接】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),仅供参考