十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Cherry Studio 知识库后端实现解析:分层架构、JobManager 调度与增删改查链路

Cherry Studio 知识库后端实现解析:分层架构、JobManager 调度与增删改查链路 Cherry Studio 知识库后端实现解析分层架构、JobManager 调度与增删改查链路【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio导读本文基于 Cherry Studio 开源仓库中 知识库后端当前实现说明 的核心内容结合当前主进程实际落地的src/main/features/knowledge模块源码系统讲解知识库后端的分层架构、职责边界、JobManager 持久化调度模型以及 add / delete / reindex / search / chunk 等关键链路的真实行为。读完本文你将掌握 Cherry Studio 知识库后端“数据服务 工作流编排 持久化任务调度”三权分立的设计思路理解knowledge_item.status八个状态的语义与补偿机制并能够对照源码定位每一个关键调用的实现位置。说明v2-refactor-temp目录下的文档属于临时工作笔记本文事实以 Knowledge Service、Knowledge Workflow Architecture、Knowledge Operation Guards 三份 canonical 文档与当前源码为准。文中涉及旧版src/main/knowledge或knowledge-base:*通道的内容不在本文覆盖范围。1. 整体架构四条调用路径与三权分立当前知识库后端采用“轻量工作流 持久化任务队列”的模型而不是单条索引管道。整体架构可归纳为下图UI / preload IPC / main-side workflow - KnowledgeService - KnowledgeIngestionService写侧编排 - JobManager - knowledge.prepare-root - knowledge.index-documents - knowledge.delete-subtree - knowledge.reindex-subtree - KeyedMutex.runExclusivebase 级变更锁 - KnowledgeBaseService / KnowledgeItemService - KnowledgeVectorStoreService / FileManager UI Data API reads / patch - Data API knowledge handlers - KnowledgeBaseService / KnowledgeItemService关键事实当前没有KnowledgeRuntimeService也没有 Knowledge 模块自己维护的 in-memory queue。持久化调度、retry、timeout、cancel 和 startup recovery 全部由JobManager负责knowledge-backend-decisions.md 第 1 节、workflow-architecture.md。在源码层面入口是 KnowledgeService.ts它是一个Injectable(KnowledgeService)ServicePhase(Phase.WhenReady)的生命周期服务构造时创建KeyedMutex即文档中的KnowledgeLockManager、KnowledgeIngestionService、KnowledgeBaseAdminService、KnowledgeQueryService、KnowledgeConceptServiceonInit()向 JobManager 注册 5 个 job handlerknowledge.prepare-root、knowledge.index-documents、knowledge.check-file-processing-result、knowledge.delete-subtree、knowledge.reindex-subtreeonAllReady()启动时执行recoverDeletingItems()与recoverInterruptedItems()两类恢复逻辑它本身不持有任何领域逻辑所有公开方法直接委托给对应子模块是一个典型的“门面facade”。三条职责带可以概括为职责带代表模块负责内容数据持久层KnowledgeBaseService/KnowledgeItemServiceSQLite 业务表读写、status/error 持久化、type/data 校验、容器状态聚合编排层KnowledgeIngestionService写侧KnowledgeQueryService读侧add/delete/reindex 准入与分派、search/chunk 查询执行层5 个 JobManager handler KnowledgeVectorStoreService读取、切分、embedding、向量写入、子树清理2. 当前职责边界谁做什么、谁不做什么文档第 2 节给出了非常明确的职责清单这与源码结构完全对应knowledge 目录结构KnowledgeBaseService/KnowledgeItemService数据服务层负责 SQLite 业务表读写负责knowledge_item.status/error的持久化更新负责knowledge_item.data与type的一致性校验负责 container item 的子项状态向上聚合不负责reader、embedding、向量库写入、JobManager 调度或 caller-facing IPC。Data API knowledge handlers只暴露数据库可直接满足的读操作与 base metadata/config 更新GET /knowledge-bases、GET /knowledge-bases/:id、PATCH /knowledge-bases/:id、GET /knowledge-bases/:id/items、GET /knowledge-items/:id不负责runtime mutation不创建或删除 vector store artifacts。KnowledgeService负责 caller-facing 的knowledge-runtime:*IPC负责 create/delete/restore base 工作流注册 Knowledge JobManager handlers持有KnowledgeIngestionService与KeyedMutex对 delete / reindex / chunk 操作做入口 guard不直接执行reader / chunk / embed / vector write。KnowledgeIngestionService写侧编排源码负责addItems/deleteItems/reindexItems的 workflow 分支负责scheduleItem(baseId, itemId)/scheduleIndexing/scheduleFileProcessingCheck将directory分派为knowledge.prepare-root将file/note/url分派为knowledge.index-documents负责 add/reindex 调度失败后的状态补偿。变更锁KeyedMutex负责同一 base 下的进程内 mutation 串行化保护 vector replace/delete、FileRef cleanup、item status writes 与 destructive cleanup/reset不能替代DbService.withWriteTx——主 SQLite 写事务仍必须走DbService.withWriteTx或等价db.transaction()变更锁只是进程内串行化手段不提供跨存储原子性。3. 调用边界与调用方契约payload-based 添加模型3.1 UI / preload 的调用模型文档第 3 节给出了 UI 侧两条调用路径UI | -- Data API | - list/get knowledge bases | - patch base metadata/config | - list/get knowledge items | \-- preload knowledgeRuntime IPC - create/delete/restore base - add/delete/reindex items - search - list/delete chunks对应的 IPC 路由定义在 knowledge.ts包括knowledge.create_base/knowledge.restore_base/knowledge.delete_baseknowledge.add_items/knowledge.delete_items/knowledge.reindex_itemsknowledge.enable_embedding_modelknowledge.search/knowledge.get_file_path/knowledge.list_item_chunks从 IPC schema 的注释可以看到两个重要契约细节一是这些路由只有 Request block、没有 Event block——索引进度不是通过 IPC 事件推送的而是渲染进程通过 DataApi 轮询knowledge_item.status获取二是delete_items与reindex_items共用itemIdsInputSchema输入为{ baseId, itemIds }且itemIds有min(1).max(KNOWLEDGE_RUNTIME_ITEMS_MAX)的硬上限。3.2 payload-based 添加不要先建行再传 id文档明确强调了一条容易踩坑的契约调用方不应先通过 Data API 创建 item再把 created item ids 传给 runtimeaddItems。正确的做法是一次调用携带完整 payload由 workflow 创建knowledge_item行并排队任务caller - preload IPC add-items(item payloads)add-items接受KnowledgeAddItemInput[]内部经KnowledgeAddItemInputSchemazod 校验在根行创建、首批任务入队后即返回——返回不代表索引完成后续进度通过 item status 轮询观察。3.3 Leaf 与 Container 两条链路Leaf itemfile/note/url当前链路add-items(leaf payloads) - create leaf item rows - status processing - enqueue knowledge.index-documentsContainer itemdirectory当前链路add-items(directory payloads) - create root item rows - status preparing - enqueue knowledge.prepare-root - prepare-root expands owner - prepare-root creates child rows - workflowService.scheduleItem(child)prepare-root创建出的 child可以继续是directory由 workflow service 再次分派为knowledge.prepare-root——递归展开由编排层循环处理而不是由 reader 或 leaf indexing 分支负责workflow-architecture.md 的 Recursive Container Expansion 一节。4. JobManager 模型每 base 独立队列 幂等键4.1 Job types 与队列命名当前 Knowledge job types 在 jobTypes.ts 中以 TypeScript module augmentation 方式注册payload 结构如下Job typepayloadknowledge.prepare-root{ baseId, itemId }knowledge.index-documents{ baseId, itemId }knowledge.check-file-processing-result{ baseId, itemId, fileProcessingJobId, pollRound, firstScheduledAt, processedRelativePath }knowledge.delete-subtree{ baseId, rootItemIds }knowledge.reindex-subtree{ baseId, rootItemIds }每个 base 使用独立队列队列名由 types.ts 中的knowledgeQueueName(baseId)生成base.${baseId}JobManager 负责 job 持久化、dispatch、retry / timeout、cancel 与 startup recovery。Knowledge 模块不再维护entriesmap、controller、runPromise、interruptError等 in-memory queue 状态。幂等键同样定义在 types.tsknowledge:${baseId}:${sorted root ids}:delete knowledge:${baseId}:${sorted root ids}:reindex4.2 任务级默认策略源码证据以 indexDocumentsJobHandler.ts 为例job 默认策略展示了重试与超时的真实取值recovery: abandon, // 应用重启不静默恢复避免重复消耗付费 embedding API defaultQueue: (input) knowledgeQueueName(toKnowledgeBaseId(input.baseId)), defaultConcurrency: 5, defaultRetryPolicy: { maxAttempts: 3, backoff: exponential, baseDelayMs: 1000, maxDelayMs: 30_000 }, defaultTimeoutMs: 30 * 60 * 1000, // 30 分钟超时这里recovery: abandon是 crash 语义的关键索引类 handler 在应用重启后不会自动恢复被中断的 item 会在启动时被recoverInterruptedItems()置为failed等待用户手动 reindex只有knowledge.delete-subtree使用recovery: retry保证未完成的删除清理能被 JobManager 启动恢复继续执行operation-guards.md 的 Shutdown 一节。5. 索引执行链路knowledge.index-documents详解文档第 5 节给出了索引 job 的完整流程handler.execute - load base and item - skip missing / deleting / already completed item - under base mutation lock: rebuild source file refs status reading - read documents - chunk documents - under base mutation lock: status embedding - embed chunks - under base mutation lock: re-read item skip vector write if item is deleting vectorStore.replaceByExternalId(itemId, nodes) status completed对照源码可以进一步确认两个实现细节状态写入与锁的关系reading与embedding状态的写入发生在 base mutation lock 之外indexDocumentsJobHandler.ts 中注释说明这只写主应用 DB 的knowledge_item不写锁保护的 per-baseindex.sqlite且KnowledgeItemService.updateStatus自身的deletingguard 已覆盖锁要防的竞态而向量替换replaceByExternalId与最终completed写入在锁内完成。零 chunk 也视为成功如果 reader 返回空 documents或 chunk 后没有可索引 chunksindex-documents仍视为成功——写入replaceByExternalId(itemId, [])清空该 item 的旧 chunks然后标记completed。但源码中有一个更细的分支当 chunk 结果为空时job handler 会抛出EMPTY_INDEXABLE_TEXT_ERRORNo indexable text was extracted...使 item 进入failed而不是completed——这是为了拒绝扫描/纯图片 PDF 看起来可搜索但实际没有可索引文本的情况。这与空 documents 仍成功的差异在于完全没读到文档 vs 读到了但没有可索引文本。非中断错误由 JobManager retry重试耗尽或 job cancel 时handler 的onSettledsettled.ts把对应 item 标记为failed但如果 item 已经是deleting则跳过失败回写。另外源码展示了 embedding 进度如何暴露给 UIhandler 通过reportKnowledgeProgress(ctx, percent, { stage, currentFile, totalFiles })上报进度并将进度百分比写入 CacheServicekey 为knowledge.item.embedding_progress.${itemId}纯内存、不持久化、退出后 60 秒 TTL 回收渲染层通过轮询 item status 感知阶段变化。6. Delete / Reindex 链路两段式与终态准入6.1delete-items先标记、后清理delete-items(baseId, itemIds) - collapse to top-level roots - under base mutation lock: mark selected root subtrees deleting - enqueue knowledge.delete-subtree其中折叠到顶层根由KnowledgeItemService.getOutermostSelectedItemIds完成去重、校验 baseId 归属、移除已被祖先选中的后代、防止同一子树在一次请求中被重复操作operation-guards.md。knowledge.delete-subtreejob 执行- resolve still-deleting subtree - cancel active jobs touching subtree - under base mutation lock: delete vectors for leaf items clear Knowledge FileRef rows for full subtree delete knowledge_item rows一个重要的数据建模事实file_ref.sourceId是 polymorphic没有 FK 指向knowledge_item。因此最终 hard delete 必须先展开完整 subtree 清理 Knowledge FileRef再删除 rows仅删除显式 root id 对应的 refs 会留下 descendant orphan refs。删除的原子性语义operation-guards.md 的deleteItems一节deleting状态写入与knowledge.delete-subtree入队共享同一事务若入队失败事务回滚rows 保持原状态仍对用户可见不存在需要启动恢复的已提交删除意图启动时onAllReady的recoverDeletingItems()会以 500 个根为一组DELETE_RECOVERY_ROOT_CHUNK_SIZE扫描已提交的deleting根并 best-effort 重新入队清理 job——这覆盖的是已入队清理中断/失败的情况而不是同步入队失败的兜底删除清理失败不把 rows 转成faileddeleting是隐藏请求删除内容的状态默认列表、搜索、RAG 读取都会排除failed意味着索引进程失败且可能被视为可见用户数据若向量未清完就把deleting - failed会令陈旧 chunks 重新可搜索。6.2reindex-items仅限终态子树reindex-items(baseId, itemIds) - collapse to top-level roots - reject unless every selected subtree item is completed or failed - enqueue knowledge.reindex-subtreeknowledge.reindex-subtreejob 执行- skip if delete already marked any subtree item deleting - under base mutation lock: re-check deleting guard delete old vectors delete expanded descendants for selected container roots reset selected roots to preparing / processing - workflowService.scheduleItem(root)源码中REINDEX_ALLOWED_STATUSES new Set([completed, failed])是这条规则的直接实现KnowledgeIngestionService.ts。设计意图operation-guards.md 的reindexItems一节Reindex 不是 cancellation primitiveactive subtreeidle/preparing/processing/reading/embedding只能 delete不能 reindex。允许对 active 子树 reindex 会重新引入取消竞态——旧 job 可能还在读源/写向量/展开子项而 reindex job 同时在删向量、重置行失败项可以 reindex因为failed已是终态deleting项不可 reindex因为 delete 一旦写入持久删除意图就接管了清理Delete 总是赢reindex 在入队前拒绝deletingreindex-subtree在 job 入口与变更锁内还会二次检查deleting覆盖入队后、执行前的时间窗一旦 delete 胜出reindex 直接跳过绝不把 deleting 行改回preparing/processingreindex 入口在入队前不预写 active 状态因此入队失败可直接上抛而不会留下卡死的 active 行但 job 内的 reset mutation 会在scheduleItem之前把根置为preparing/processing若后续调度失败handler 会执行补偿把未完成调度的根标记为failed保持 UI 诚实用户触发的 reindex 立即可见为活动中的任务。6.3 与旧文档的差异提示需要提醒读者v2-refactor-temp下的 knowledge-schema.md 是一份更早的笔记其中还提到KnowledgeRuntimeService与基于 in-memoryp-queue的index-leafjob type——这些描述已过时。当前实现以 KnowledgeService.ts 注册的 5 个 job type 为准knowledge.index-documents取代了index-leaf调度完全由 JobManager 持久化驱动。7.knowledge_item.status边界八状态生命周期当前status表达业务生命周期和粗粒度运行进度共 8 个值knowledge-backend-decisions.md 第 7 节源码 knowledge.ts 中KnowledgeItemStatus状态语义idle初始/未开始preparingdirectory正在 expand / create childrenprocessingleaf 已接受但尚未进入 reading或 container 仍有 active childrenreadingleaf 正在读取 source documentsembeddingleaf 正在 embeddingcompletedleaf indexing 完成或 container 没有 active childrenfailedindex / preparation / scheduling compensation 失败deleting用户不可见等待后台 cleanup关键原则不再保留单独的phase字段——preparing、reading、embedding都是一等公民的status值status是持久业务状态不由 JobManager progress 反推——JobManager 的 progress 只是诊断性的执行状态不是 item 生命周期的真源source of truthcontainer 状态由直接子项状态聚合且聚合时必须考虑deleting子项会被容器 reconcile 忽略——这正是listItemChunks对 completeddirectory还要检查 subtree 是否含deleting后代的原因任何非删除的子树状态更新都必须把子树外部的祖先容器纳入 reconcile 范围例如子子树调度失败变failed后父 directory 不能停留在没有活动工作的processing子树成员关系解析必须与状态写入处于同一个串行化写事务内避免并发 create/delete 造成成员关系漂移operation-guards.md 的 Subtree Status Reconciliation。8. Base Workflowcreate / delete / restore8.1createBase(dto)IPC create-base(CreateKnowledgeBaseDto) - KnowledgeBaseService.create(dto) - KnowledgeVectorStoreService.createStore(base) - return created base如果 vector store 初始化失败orchestration 会调用KnowledgeBaseService.delete(base.id)回滚刚创建的 SQLite base然后把原始错误抛给调用方——保证两个存储SQLite 向量存储要么一起成功要么一起回滚到没有 base的状态。8.2deleteBase(baseId)IPC delete-base(baseId) - cancel active Knowledge jobs in base queue - under base mutation lock: KnowledgeVectorStoreService.deleteStore(baseId) KnowledgeBaseService.delete(baseId)失败语义是对称的Artifact 删除失败时 SQLite 行保留用户可从 UI 重试SQLite 删除失败时已删除 artifacts 不会恢复orchestration 抛出invalidOperation——因为跨存储的清理无法原子回滚。8.3restoreBase(dto)IPC restore-base(sourceBaseId, embeddingModelId, dimensions) - load source base - load source root items - create new base with source config and requested embedding contract - add source root item payloads to restored baseRestore 的准入规则与边界knowledge-service.md 的 Base Restore允许 failed base也允许 completed base即使 completed source base 的embeddingModelId和dimensions未变化也允许创建同配置 clone/rebuild——同配置 restore 是合法的克隆/重建工作流不是 no-op不会被拒绝只复制 root itemsgroupId null展开的 directory 子项不复制——它们属于旧 base 层级可由新 base 的 container preparation 流程重新生成dimensions必须是针对所选embeddingModelId已解析的正整数后端不做二次模型探测若与实际 embedding 输出宽度不匹配会在后续 indexing/写向量阶段暴露restore 前会对每个 source root 探测源确认缺失的 root 被跳过并计入skippedMissingSourceCount若已接受的 root 无法添加orchestration best-effort 删除新 base 并重抛invalidOperation旧 failed base 保留不动由产品/UI 层决定保留确认还是 restore 成功后删除。8.4 可恢复的迁移失败 basev1 → v2 迁移时如果遗留 base 引用的 embedding model 在迁移后的user_model表中不存在例如ollama::dengcao/Qwen3-Embedding-0.6B:Q8_0迁移不能丢弃用户数据而是knowledge_base.embeddingModelId null、dimensions 有效遗留维度未知则为 nullknowledge_base.status failed、error missing_embedding_model该 base 下knowledge_item行继续迁移遗留向量跳过没有确认的 embedding 契约如果模型可解析但遗留向量库缺失/为空/被锁/无效则保留embeddingModelId、dimensions null、标记failed/missing_vector_store。knowledge_base.error是共享的KnowledgeBaseErrorCode枚举值而非自由字符串。两种失败的恢复路径都是knowledge.restore_base用户选择有效 embedding model 后重建新 base而不是原地修复。9. Search / Chunk 边界9.1search(baseId, query)文档第 9 节与 canonical 文档给出完全一致的流程拒绝 failed base拒绝没有 searchable token 的 queryIPC schema 层面query还需trim().min(1).max(1000)使用 base embedding model 生成 query embedding无 embedding model 的 base 只走 BM25embedding-backed base 走 hybrid 检索查询 sqlite-vec vector storeKnowledgeIndexStore.tssearch_text_fts的 BM25 通道 LIKE fallback 处理短 CJK token或 BM25 与暴力向量结果 RRF 融合候选量topK × overfetch有上限过滤 missing / other-base / deleting source item 的结果裁剪到documentCount ?? 10如果配置了rerankModelId执行 rerank仅对scoreKind relevance的结果应用 relevance thresholdBM25 / hybrid 的ranking分数不做 threshold 过滤然后写入 rank。KnowledgeSearchResult的关键字段chunkIdsearch unit 身份search_unit.unit_id必填、itemId等于 unit 的material_id即knowledge_item.id可选、conceptIdmaterial 在 base index 内的相对路径供 agent 通过kb_read/kb_manage定位同一文档可选。9.2 Chunk 操作规则list-item-chunks/delete-item-chunk注意canonical 文档已明确没有 chunk-delete mutation——chunks 是派生索引行由 reindex 整体重建当前规则拒绝 failed base要求目标 item 自身为completed对 completeddirectory的 list 请求如果 subtree 仍含deletingdescendant则拒绝——因为容器 reconcile 忽略deleting子项容器可能显示completed而底下清理仍在进行。10. 当前明确不做的内容Non-Goals文档第 10 节列出的边界清单也是排查历史代码时最实用的排除项不使用旧KnowledgeRuntimeService不维护 Knowledge 自己的 in-memory queue调度归 JobManager不使用index-leafjob type——当前 leaf indexing job 是knowledge.index-documents不保留单独phase字段不把 restore same embedding config 视为 no-op 并拒绝不主动 permanent-delete detachedFileEntryrows历史FileEntry行留给文件模块的 no-reference 策略处理workflow-architecture.mdRound 1 不接入 FileProcessing——knowledge_base.fileProcessorId在源规划之外当前对 indexing inert不过源码中已存在knowledge.check-file-processing-resultjob 与FILE_PROCESSING_CHECK_DELAY_MS 5_000、FILE_PROCESSING_MAX_WAIT_MS 30 * 60 * 1000的轮询实现说明 FileProcessing 通路已具备执行骨架可作为后续演进方向阅读。另外从检索成本角度有一个明确的实现边界knowledge-service.md 的 Current Retrieval Cost Assumption当前实现不创建向量索引、不使用 ANN 近似最近邻查找相似度搜索直接扫描embedding行并按vec_distance_cosine排序查询向量以 raw little-endian float32 BLOB 绑定。这意味着单个 base 的检索成本随向量行数近似线性增长——这是确切的实现边界而非可扩展性承诺。11. 扩展阅读与源码导航围绕本文主题建议按以下顺序深入后端总览Knowledge Service本文事实的 canonical 来源工作流架构Knowledge Workflow Architecture调度模型、递归展开、crash 语义操作守卫Knowledge Operation Guards四类变更操作的失败语义与 review checklist门面入口KnowledgeService.tsjob 注册、启动恢复、方法委托写侧编排KnowledgeIngestionService.tsadd/delete/reindex 准入、冲突策略、补偿Job payload 定义jobTypes.ts5 个 job 的持久化输入结构索引 job 实现indexDocumentsJobHandler.tsreading → embedding → completed 状态机与重试策略IPC 路由契约knowledge.tsrenderer→main 的 zod 校验边界集成测试KnowledgeService.integration.test.ts、addConflicts.test.ts、indexDocumentsJobHandler.test.ts本文档对应的临时笔记 knowledge-backend-decisions.md 还给出了后续维护原则同样适用于阅读代码时的判断标准如果 canonical 文档已覆盖事实优先以 canonical 为准区分当前实现与目标设计避免将 RFC 目标误读为已落地行为。【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址: https://gitcode.com/CherryHQ/cherry-studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表