news 2026/9/12 4:00:13

Karakeep 系统架构解析:SQLite 任务队列驱动的 Web 应用与三类后台 Worker

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Karakeep 系统架构解析:SQLite 任务队列驱动的 Web 应用与三类后台 Worker

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 供全文检索。

一、整体架构:三大角色的职责划分

架构文档用极简的几行文字勾勒出整个系统的骨架,其核心可以拆解为三个角色:

角色技术栈职责
WebappNext.js + SQLite面向用户的前端界面与数据持久化层
WorkersNode.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注册了crawlerlowPriorityCrawlerembeddingsinferencesearchadminMaintenancevideofeedassetPreprocessingwebhookruleEnginebackup共十余种 Worker;
  • WORKERS_ENABLED_WORKERS/WORKERS_DISABLED_WORKERS两个环境变量(逗号分隔的 Worker 名单)可精确控制启用/禁用哪些 Worker,见 config.ts;
  • 所有 Worker 通过getQueueClient().createRunner()pollIntervalMs: 1000(每秒轮询一次)、可配置concurrencytimeoutSecs的方式运行。

三、三类核心 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_ONDEMANDfalse是否按需建立浏览器连接
BROWSER_COOKIE_PATH浏览器 Cookie 文件路径(用于登录态抓取)
CRAWLER_NUM_WORKERS1并发爬取 Worker 数
CRAWLER_JOB_TIMEOUT_SEC60单个爬取任务超时时间(秒)
CRAWLER_NAVIGATE_TIMEOUT_SEC30页面导航超时(秒)
CRAWLER_STORE_SCREENSHOTtrue是否保存截图
CRAWLER_FULL_PAGE_SCREENSHOTfalse是否保存整页长截图
CRAWLER_STORE_PDFfalse是否另存 PDF
CRAWLER_FULL_PAGE_ARCHIVEfalse是否生成整页归档(SingleFile/Monolith)
CRAWLER_VIDEO_DOWNLOADfalse是否尝试下载页面视频
CRAWLER_ENABLE_ADBLOCKERtrue是否启用广告拦截器
CRAWLER_ENABLE_AUTOCONSENTtrue是否自动同意 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):

  1. 解析请求:用zCrawlLinkRequestSchema校验任务数据,提取bookmarkIdarchiveFullPagestorePdf
  2. 限流检查checkDomainRateLimit()按目标域名调用限流客户端,被限流时抛出QueueRetryAfterError,并以 1.0~1.4 的随机抖动延迟重试,避免"惊群效应"(crawlerWorker.ts);
  3. 探测内容类型getContentTypeAndMetadata()预检 URL 的 Content-Type,若是 PDF 或受支持的图片类型,则走handleAsAssetBookmark()将其作为资产书签(asset bookmark)处理,而非网页抓取;
  4. 浏览器抓取与解析crawlAndParseUrl()执行真正的浏览器渲染、HTML 解析(parseSubprocess.ts子进程解析以隔离内存占用)、正文提取与元数据写入;
  5. 入队后续任务enqueuePostCrawlJobs()在抓取成功后按需入队推理(打标/摘要/Embedding)、搜索重建索引、视频下载和crawledWebhook 等下游任务(crawlerWorker.ts);
  6. 归档:最后执行截图/PDF/整页归档等可能失败的归档逻辑,成功与否通过onComplete/onError回写bookmarkLinks.crawlStatussuccess/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_KEYOpenAI(或兼容服务)API Key
OPENAI_BASE_URL自定义 API Base URL(对接兼容服务)
OLLAMA_BASE_URLOllama 本地模型地址
INFERENCE_TEXT_MODELgpt-5.6-luna文本推理模型
INFERENCE_IMAGE_MODELgpt-4o-mini图片推理模型
INFERENCE_ENABLE_AUTO_TAGGINGtrue是否启用自动打标
INFERENCE_ENABLE_AUTO_SUMMARIZATIONfalse是否启用自动摘要
INFERENCE_LANGenglish打标语言偏好
INFERENCE_NUM_WORKERS1推理并发数
INFERENCE_JOB_TIMEOUT_SEC30推理任务超时
INFERENCE_CONTEXT_LENGTH2048输入上下文长度
INFERENCE_MAX_OUTPUT_TOKENS2048最大输出 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):包含书签iduserId、链接型书签的url/linkTitle/description/ 纯文本正文content/publisher/author/ 发布时间,资产型书签的contentmetadata,文本型书签的text,以及公共字段notesummarytitlecreatedAttags

搜索客户端与索引配置: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 串起来,一条链接书签从用户点击收藏到出现在搜索结果中,完整链路如下:

  1. 入库:Web App 通过 API / TRPC 创建书签记录并写入 SQLite(数据模型见 packages/db/schema.ts);
  2. 入队爬取:创建书签时向LinkCrawlerQueue(或低优先级队列LowPriorityCrawlerQueue)入队ZCrawlLinkRequest
  3. 爬取:Crawling Worker 每秒轮询取任务,经域名限流、内容类型探测后,用无头 Chrome 渲染页面并解析正文与元数据,产物写入 SQLite(正文资产、截图等),随后级联入队推理与搜索重建任务(enqueuePostCrawlJobs,crawlerWorker.ts);
  4. 推理:Inference Worker 消费OpenAIQueue,调用 AI 模型生成标签(必要时先生成 Embedding 再打标)与摘要,回写bookmarks.taggingStatus/summarizationStatus
  5. 索引:Search Worker 消费SearchIndexingQueue,将书签组装为搜索文档批量写入 Meilisearchbookmarks索引;
  6. 检索:用户在前端输入关键词,Web App 调用 Meilisearch 完成全文检索(过滤条件按userId隔离,避免跨用户数据泄露)。

其中第 3、4 步之间通过队列优先级传播(enqueueOpts.priority = job.priority,crawlerWorker.ts)保证新收藏的高优先级书签优先被处理,同时每次运行都会记录埋点指标(workerStatsCounterbookmarkCrawlLatencyHistogram),便于观测整条流水线的健康度。

五、架构的可扩展性与容错设计

虽然架构文档只提到了三类任务,但实际仓库中的 Worker 体系已经远远不止这些。从 apps/workers/index.ts 可以看到还包括:embeddings(向量化)、video(视频下载)、feed(RSS 订阅刷新)、webhook(Webhook 投递)、backup(定时备份)、assetPreprocessing(资产预处理)、ruleEngine(规则引擎)、adminMaintenance(管理维护)等。它们共享同一套队列抽象,因此新增一类任务只需实现"入队类型 + Runner 回调 + Worker 构建器",这正是插件化队列设计的价值所在。

容错方面值得关注的设计点:

  • 失败重试与状态回写:爬取失败且重试耗尽时,onError会在一个数据库事务里把crawlStatus置为failure,并清理taggingStatussummarizationStatusembeddingStatus中残留的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.tssummarize.ts);
  • 搜索索引 Worker 实现:apps/workers/workers/searchWorker.ts,Meilisearch 插件:packages/plugins/search-meilisearch/src/index.ts;
  • 队列抽象与插件:packages/shared/queueing.ts、packages/plugins(queue-litequequeue-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_WORKERSINFERENCE_NUM_WORKERSSEARCH_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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 3:57:44

关税波动下跨境电商履约成本管控与利润守住的实战方法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 3:56:52

GDB调试器入门:从基础到高级技巧

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 3:56:49

基于Flask的游泳馆管理系统开发实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华