Karakeep 系统架构解析:SQLite 任务队列驱动的 Web 应用与三类后台 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
导读
本文基于 04-architecture.md(v0.32.0 版本文档,当前仓库docs/versioned_docs/version-v0.32.0/08-development/04-architecture.md亦含同内容),深入解析 Karakeep(Hoarder 开源项目的新名)的整体架构:一个以Next.js 构建的 Web 应用(Webapp)作为前端入口,以SQLite 同时承担数据存储与任务队列的角色,配合多组后台 Worker 异步消费任务,其中三类核心 Worker(爬取 Crawling、推理 Inference、索引 Indexing)分别完成网页抓取、AI 自动打标与全文检索索引。读完本文,你将掌握该项目的核心数据流、三类任务的执行链路、关键环境变量配置,以及各模块在仓库中的源码位置,为二次开发或自托管调优提供清晰的架构地图。
Karakeep 总体架构图:Web App 与 Workers 通过 SQLite 持久化数据,Workers 借助无头浏览器(Headless Browser)抓取网页,并将解析内容索引到 Meilisearch 供全文检索。
一、整体架构:三大角色的职责划分
架构文档用极简的几行文字勾勒出整个系统的骨架,其核心可以拆解为三个角色:
| 角色 | 技术栈 | 职责 |
|---|---|---|
| Webapp | Next.js + SQLite | 面向用户的前端界面与数据持久化层 |
| Workers | Node.js 常驻进程(同仓库apps/workers) | 从 SQLite 任务队列消费作业并执行 |
| Meilisearch | 独立搜索引擎服务 | 为书签内容建立全文索引,支撑快速检索 |
从 docker-compose.yml 可以看出生产部署形态:web服务是唯一的用户入口(暴露3000端口),chrome服务为无头浏览器(BROWSER_WEB_URL: http://chrome:9222),meilisearch服务提供搜索能力(MEILI_ADDR: http://meilisearch:7700)。Web 服务同时通过DATA_DIR: /data将 SQLite 数据文件持久化到磁盘卷。也就是说,SQLite 数据文件、无头浏览器和 Meilisearch 都以独立服务的形式存在,而 Workers 作为web镜像内部的子进程运行——这正是"单容器"部署(Monolith)形态的基础。
从源码结构看,Web 应用本体位于 apps/web(Next.js App Router),API 路由与 TRPC 层位于 packages/api 与 packages/trpc,数据模型(Drizzle ORM Schema)位于 packages/db/schema.ts,Worker 进程则整体位于 apps/workers。
二、SQLite 的双重身份:数据仓库 + 任务队列
架构文档特别强调"基于 SQLite 的任务队列"(sqlite based job queue),这是本项目区别于常规"Redis + 消息队列"方案的最大特色:同一个 SQLite 数据库文件既保存业务数据,又充当作业队列的持久化存储。这带来的直接好处是部署极简——不需要额外维护 Redis 或专用队列服务,单文件数据库天然支持备份与迁移(仓库snapshots/目录即存放着 seed 数据快照)。
任务队列的抽象定义在 packages/shared/queueing.ts:
Queue接口:enqueue(payload, options)入队、stats()查询 pending / running / failed 等队列状态;Runner接口:通过run(job)执行任务、onComplete(job, result)处理成功、onError(job)处理失败;EnqueueOptions支持priority(优先级)、groupId(分组)、delayMs(延迟执行)等调度参数;- 特殊错误
QueueRetryAfterError用于"限流后稍后重试",且不计入重试次数上限(见 queueing.ts)。
队列的实际实现由插件系统提供。getQueueClient()通过PluginManager.getClient(PluginType.Queue)获取队列客户端(queueing.ts),仓库 packages/plugins 下既有queue-liteque(基于 SQLite 的轻量队列),也有queue-restate(基于 Restate 的分布式队列)等实现可选。
而任务入队与消费的"总装配"在 apps/workers/index.ts:
workerBuilders注册了crawler、lowPriorityCrawler、embeddings、inference、search、adminMaintenance、video、feed、assetPreprocessing、webhook、ruleEngine、backup共十余种 Worker;WORKERS_ENABLED_WORKERS/WORKERS_DISABLED_WORKERS两个环境变量(逗号分隔的 Worker 名单)可精确控制启用/禁用哪些 Worker,见 config.ts;- 所有 Worker 通过
getQueueClient().createRunner()以pollIntervalMs: 1000(每秒轮询一次)、可配置concurrency与timeoutSecs的方式运行。
三、三类核心 Worker 深入解析
架构文档列出的三类任务(Crawling / OpenAI / Indexing)正是数据从"收藏"到"可检索"全流程的三个环节。下面结合源码逐一展开。
3.1 Crawling Worker:无头浏览器抓取网页
职责:接收爬取任务,使用运行在 workers 容器中的无头 Chrome 浏览器获取链接内容,并产出正文、截图、PDF、元数据等资产。
入口与调度:crawlerWorker.ts 中CrawlerWorker.build()通过getQueueClient().createRunner<ZCrawlLinkRequest, CrawlerRunResult>()注册执行器,其核心参数来自配置(config.ts):
| 环境变量 | 默认值 | 说明 |
|---|---|---|
BROWSER_WEB_URL/BROWSER_WEBSOCKET_URL | 无 | 无头浏览器地址(HTTP / WebSocket 两种连接方式) |
BROWSER_CONNECT_ONDEMAND | false | 是否按需建立浏览器连接 |
BROWSER_COOKIE_PATH | 无 | 浏览器 Cookie 文件路径(用于登录态抓取) |
CRAWLER_NUM_WORKERS | 1 | 并发爬取 Worker 数 |
CRAWLER_JOB_TIMEOUT_SEC | 60 | 单个爬取任务超时时间(秒) |
CRAWLER_NAVIGATE_TIMEOUT_SEC | 30 | 页面导航超时(秒) |
CRAWLER_STORE_SCREENSHOT | true | 是否保存截图 |
CRAWLER_FULL_PAGE_SCREENSHOT | false | 是否保存整页长截图 |
CRAWLER_STORE_PDF | false | 是否另存 PDF |
CRAWLER_FULL_PAGE_ARCHIVE | false | 是否生成整页归档(SingleFile/Monolith) |
CRAWLER_VIDEO_DOWNLOAD | false | 是否尝试下载页面视频 |
CRAWLER_ENABLE_ADBLOCKER | true | 是否启用广告拦截器 |
CRAWLER_ENABLE_AUTOCONSENT | true | 是否自动同意 Cookie 弹窗 |
CRAWLER_DOMAIN_RATE_LIMIT_WINDOW_MS/CRAWLER_DOMAIN_RATE_LIMIT_MAX_REQUESTS | 无 | 按域名限流的窗口与请求上限 |
CRAWLER_HTTP_PROXY/CRAWLER_HTTPS_PROXY/CRAWLER_NO_PROXY | 无 | 爬取代理配置(逗号分隔) |
执行链路(runCrawler,crawlerWorker.ts):
- 解析请求:用
zCrawlLinkRequestSchema校验任务数据,提取bookmarkId、archiveFullPage、storePdf; - 限流检查:
checkDomainRateLimit()按目标域名调用限流客户端,被限流时抛出QueueRetryAfterError,并以 1.0~1.4 的随机抖动延迟重试,避免"惊群效应"(crawlerWorker.ts); - 探测内容类型:
getContentTypeAndMetadata()预检 URL 的 Content-Type,若是 PDF 或受支持的图片类型,则走handleAsAssetBookmark()将其作为资产书签(asset bookmark)处理,而非网页抓取; - 浏览器抓取与解析:
crawlAndParseUrl()执行真正的浏览器渲染、HTML 解析(parseSubprocess.ts子进程解析以隔离内存占用)、正文提取与元数据写入; - 入队后续任务:
enqueuePostCrawlJobs()在抓取成功后按需入队推理(打标/摘要/Embedding)、搜索重建索引、视频下载和crawledWebhook 等下游任务(crawlerWorker.ts); - 归档:最后执行截图/PDF/整页归档等可能失败的归档逻辑,成功与否通过
onComplete/onError回写bookmarkLinks.crawlStatus为success/failure。
抓取成功后,任务会自动级联触发后续处理——这正是架构图中"Web App → Workers → 下游"数据流的源头。
3.2 Inference Worker:调用 AI 服务自动打标与摘要
职责:调用 OpenAI 兼容 API(也支持 Ollama 等本地模型,见OLLAMA_BASE_URL配置)对已抓取内容进行标签推断与摘要生成。
入口与调度:inferenceWorker.ts 中OpenAiWorker.build()监听OpenAIQueue,根据任务类型type: "tag" | "summarize"分发到runTagging()与runSummarization(),并通过attemptMarkStatus()把taggingStatus/summarizationStatus写回bookmarks表。
关键配置(config.ts):
| 环境变量 | 默认值 | 说明 |
|---|---|---|
OPENAI_API_KEY | 无 | OpenAI(或兼容服务)API Key |
OPENAI_BASE_URL | 无 | 自定义 API Base URL(对接兼容服务) |
OLLAMA_BASE_URL | 无 | Ollama 本地模型地址 |
INFERENCE_TEXT_MODEL | gpt-5.6-luna | 文本推理模型 |
INFERENCE_IMAGE_MODEL | gpt-4o-mini | 图片推理模型 |
INFERENCE_ENABLE_AUTO_TAGGING | true | 是否启用自动打标 |
INFERENCE_ENABLE_AUTO_SUMMARIZATION | false | 是否启用自动摘要 |
INFERENCE_LANG | english | 打标语言偏好 |
INFERENCE_NUM_WORKERS | 1 | 推理并发数 |
INFERENCE_JOB_TIMEOUT_SEC | 30 | 推理任务超时 |
INFERENCE_CONTEXT_LENGTH | 2048 | 输入上下文长度 |
INFERENCE_MAX_OUTPUT_TOKENS | 2048 | 最大输出 Token 数 |
注意inference.isConfigured的计算逻辑:!!OPENAI_API_KEY || !!OLLAMA_BASE_URL(config.ts),即只要配置了 OpenAI 系或 Ollama 任一即可启用推理。若未配置任何推理后端,runOpenAI会记录"没有推理客户端"并直接返回(inferenceWorker.ts),不会阻塞抓取主流程——推理是可选增强而非强依赖。
从爬取 Worker 的enqueuePostCrawlJobs()还可以看到推理与 Embedding 的联动:若启用了 Embedding 自动索引(EMBEDDING_ENABLE_AUTO_INDEXING,默认在 OpenAI 默认配置下为true),则先入队 Embedding 任务(类型embed,完成后触发打标runTaggingOnComplete: true),否则直接入队tag任务;摘要任务(summarize)始终独立入队(crawlerWorker.ts)。
3.3 Indexing Worker:写入 Meilisearch 加速检索
职责:将书签的结构化字段(标题、URL、正文、标签、摘要等)组装为搜索文档,写入 Meilisearch,并处理书签删除时的索引清理。
入口与调度:searchWorker.ts 中SearchIndexingWorker.build()监听SearchIndexingQueue,按任务类型type: "index" | "delete"分别执行runIndex()与runDelete()。
索引文档结构(BookmarkSearchDocument,searchWorker.ts):包含书签id、userId、链接型书签的url/linkTitle/description/ 纯文本正文content/publisher/author/ 发布时间,资产型书签的content与metadata,文本型书签的text,以及公共字段note、summary、title、createdAt、tags。
搜索客户端与索引配置:Meilisearch 集成位于插件 packages/plugins/search-meilisearch/src/index.ts:
MeiliSearchProvider.isConfigured()检查MEILI_ADDR环境变量(默认由 docker-compose 注入http://meilisearch:7700);- 初始化时自动创建
bookmarks索引(primaryKey: "id"),并确保filterableAttributes = ["id", "userId"]、sortableAttributes = ["createdAt"]符合预期(index.ts); - 写入采用
BatchingDocumentQueue批量队列(MEILI_BATCH_SIZE/MEILI_BATCH_TIMEOUT_MS可调),但重试运行(runNumber > 0)时禁用批量、直接写入以提高可靠性(searchWorker.ts); - 搜索时通过
filterToMeiliSearchFilter()把过滤条件翻译为 Meilisearch 过滤语法(=/IN [...]),排序字段为createdAt(index.ts)。
搜索索引相关配置(config.ts):SEARCH_NUM_WORKERS(默认 1)、SEARCH_JOB_TIMEOUT_SEC(默认 30)。
四、一条书签的完整生命周期:从收藏到可检索
将上述三个 Worker 串起来,一条链接书签从用户点击收藏到出现在搜索结果中,完整链路如下:
- 入库:Web App 通过 API / TRPC 创建书签记录并写入 SQLite(数据模型见 packages/db/schema.ts);
- 入队爬取:创建书签时向
LinkCrawlerQueue(或低优先级队列LowPriorityCrawlerQueue)入队ZCrawlLinkRequest; - 爬取:Crawling Worker 每秒轮询取任务,经域名限流、内容类型探测后,用无头 Chrome 渲染页面并解析正文与元数据,产物写入 SQLite(正文资产、截图等),随后级联入队推理与搜索重建任务(
enqueuePostCrawlJobs,crawlerWorker.ts); - 推理:Inference Worker 消费
OpenAIQueue,调用 AI 模型生成标签(必要时先生成 Embedding 再打标)与摘要,回写bookmarks.taggingStatus/summarizationStatus; - 索引:Search Worker 消费
SearchIndexingQueue,将书签组装为搜索文档批量写入 Meilisearchbookmarks索引; - 检索:用户在前端输入关键词,Web App 调用 Meilisearch 完成全文检索(过滤条件按
userId隔离,避免跨用户数据泄露)。
其中第 3、4 步之间通过队列优先级传播(enqueueOpts.priority = job.priority,crawlerWorker.ts)保证新收藏的高优先级书签优先被处理,同时每次运行都会记录埋点指标(workerStatsCounter、bookmarkCrawlLatencyHistogram),便于观测整条流水线的健康度。
五、架构的可扩展性与容错设计
虽然架构文档只提到了三类任务,但实际仓库中的 Worker 体系已经远远不止这些。从 apps/workers/index.ts 可以看到还包括:embeddings(向量化)、video(视频下载)、feed(RSS 订阅刷新)、webhook(Webhook 投递)、backup(定时备份)、assetPreprocessing(资产预处理)、ruleEngine(规则引擎)、adminMaintenance(管理维护)等。它们共享同一套队列抽象,因此新增一类任务只需实现"入队类型 + Runner 回调 + Worker 构建器",这正是插件化队列设计的价值所在。
容错方面值得关注的设计点:
- 失败重试与状态回写:爬取失败且重试耗尽时,
onError会在一个数据库事务里把crawlStatus置为failure,并清理taggingStatus、summarizationStatus、embeddingStatus中残留的pending状态(crawlerWorker.ts),避免下游任务悬挂; - 限流退避:域名限流通过
QueueRetryAfterError延迟重试且不消耗重试次数,配合 40% 随机抖动防止限流恢复瞬间的请求风暴; - 无搜索降级:当
MEILI_ADDR未配置时,搜索 Worker 记录"搜索未配置"并直接返回(searchWorker.ts),系统其余功能不受影响; - 无推理降级:未配置任何 AI 后端时推理任务直接跳过,抓取与归档正常完成。
六、开发与部署相关参考
- 架构总览:docs/docs/08-development/04-architecture.md(v0.32.0 版本位于 docs/versioned_docs/version-v0.32.0/08-development/04-architecture.md),架构图源文件为 docs/static/img/architecture/arch.png;
- Worker 进程装配:apps/workers/index.ts;
- 爬取 Worker 实现:apps/workers/workers/crawlerWorker.ts 及 apps/workers/workers/crawler 目录(浏览器生命周期、页面抓取、探测、解析、资产持久化等模块);
- 推理 Worker 实现:apps/workers/workers/inference/inferenceWorker.ts(打标与摘要分别位于 apps/workers/workers/inference 下的
tagging.ts与summarize.ts); - 搜索索引 Worker 实现:apps/workers/workers/searchWorker.ts,Meilisearch 插件:packages/plugins/search-meilisearch/src/index.ts;
- 队列抽象与插件:packages/shared/queueing.ts、packages/plugins(
queue-liteque、queue-restate); - 全部环境变量定义:packages/shared/config.ts;
- 容器编排:docker/docker-compose.yml(web / chrome / meilisearch 三服务);
- 本地开发启动:start-dev.sh 与 CONTRIBUTING.md。
结语
Karakeep 的架构用一个 SQLite 文件同时承担了业务存储与任务队列的双重职责,配合 Next.js Web 应用与按需启停的多组 Worker,在极简部署(单容器 + Chrome + Meilisearch)与功能完整性之间取得了很好的平衡。理解"爬取 → 推理 → 索引"这条主流水线及其容错设计,是深入阅读 apps/workers 源码、调优自托管实例(如通过CRAWLER_NUM_WORKERS、INFERENCE_NUM_WORKERS、SEARCH_NUM_WORKERS调整并发)或为项目贡献新 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),仅供参考