From 1f253643745e5ea8d5ca321204f29c7c8b19bd30 Mon Sep 17 00:00:00 2001 From: lix-2026 Date: Sun, 7 Jun 2026 01:10:31 +0800 Subject: [PATCH] feat(rag): replace LiteParse flows with LightRAG provider --- AGENTS.md | 7 +- ARCHITECTURE.md | 35 +- CURRENT_ARCHITECTURE.md | 20 +- REASONIX.md | 7 +- ...7-50-lightrag-knowledge-rag-provider-v1.md | 757 +++++ ...-local-understanding-graphrag-kernel-v1.md | 643 ++++ .../document-editor-adapter-runtime.js | 45 +- .../browser/document-resource-tab-runtime.js | 206 +- .../sidebar-filetree-command-runtime.js | 107 +- .../browser/sidebar-page-ai-target-runtime.js | 38 +- .../browser/sidebar-page-settings-runtime.js | 778 ++++- .../sidebar-tree-live-apply-runtime.js | 2 +- .../mnote-web/browser/sidebar-tree-runtime.js | 75 +- rust/crates/mnote-web/src/evidence_parse.rs | 544 +--- .../src/hermes_tools/knowledge_rag.rs | 157 + .../mnote-web/src/hermes_tools/manifest.rs | 105 +- rust/crates/mnote-web/src/hermes_tools/mod.rs | 1 + .../mnote-web/src/hermes_tools/skill.rs | 44 +- rust/crates/mnote-web/src/routes/evidence.rs | 21 +- .../mnote-web/src/routes/hermes_tools.rs | 80 +- .../mnote-web/src/routes/knowledge_rag.rs | 2671 +++++++++++++++++ .../src/routes/local_folder_events.rs | 37 +- .../src/routes/local_folder_source.rs | 181 +- rust/crates/mnote-web/src/routes/local_ocr.rs | 11 + .../src/routes/local_search_index.rs | 535 +--- rust/crates/mnote-web/src/routes/mod.rs | 41 +- rust/crates/mnote-web/src/routes/tree.rs | 4 +- rust/crates/mnote-web/src/ssr/pages/layout.rs | 86 +- rust/crates/mnote-web/src/ssr/styles.rs | 168 ++ scripts/TESTING_REFERENCE.md | 2 +- scripts/reasonix-acp-wrapper.mjs | 169 +- ...sk520-page-ai-raw-resource-target-smoke.js | 52 +- scripts/task526-local-folder-ocr-api-smoke.js | 436 --- ...document-evidence-liteparse-agent-smoke.js | 387 --- ...owledge-rag-citation-resource-tab-smoke.js | 150 + ...nowledge-rag-page-ai-final-answer-smoke.js | 277 ++ ...k532-knowledge-rag-docx-ingestion-smoke.js | 234 ++ ...knowledge-rag-source-watcher-sync-smoke.js | 235 ++ ...ledge-rag-source-management-scope-smoke.js | 217 ++ skills/mnote-knowledge-rag/SKILL.md | 17 + 40 files changed, 7232 insertions(+), 2350 deletions(-) create mode 100644 design/07-ai/done/7-50-lightrag-knowledge-rag-provider-v1.md create mode 100644 design/old/07-ai/process/7-49-local-understanding-graphrag-kernel-v1.md create mode 100644 rust/crates/mnote-web/src/hermes_tools/knowledge_rag.rs create mode 100644 rust/crates/mnote-web/src/routes/knowledge_rag.rs delete mode 100644 scripts/task526-local-folder-ocr-api-smoke.js delete mode 100644 scripts/task528-document-evidence-liteparse-agent-smoke.js create mode 100644 scripts/task529-knowledge-rag-citation-resource-tab-smoke.js create mode 100644 scripts/task530-knowledge-rag-page-ai-final-answer-smoke.js create mode 100644 scripts/task532-knowledge-rag-docx-ingestion-smoke.js create mode 100644 scripts/task533-knowledge-rag-source-watcher-sync-smoke.js create mode 100644 scripts/task534-knowledge-rag-source-management-scope-smoke.js create mode 100644 skills/mnote-knowledge-rag/SKILL.md diff --git a/AGENTS.md b/AGENTS.md index 288a3daf..7a757c92 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ - `3000` 当前主壳已接入 Rust Web WebSocket push 主链(`/api/realtime/ws`)+ SSE fallback(`/api/tree/events`)的 snapshot / delta / resync consumer,并已有 browser smoke 验证(2026-05-17 WS 迁移 `57ec8322`);后续收口重点是统一 live cache 与减少补偿链,而不是把它描述成“还没接 live stream”。 - 文档页默认主编辑器已切到页面内 `leptos-tiptap` island;`BlockNote` 已退出文档页默认主路径,只保留为历史参考实现 / 对照材料。 - Page Aggregate 当前已输出 `blockDocument / blockProjectionVersion / projectionSource`(`projectionSource=documents.content`),标题/正文/页面设置写入后 smoke 已通过(`task110`、`task-page-aggregate-body-sync-smoke`、`task-page-aggregate-options-sync-smoke`);页面/块 AI 最小工具链已通过 Rust Hermes tools 读取和写入块投影;但这仍是从 `documents.content` / local markdown content 投影出来的过渡态,不是 EditorBlockDocument 原生落库完成态。客户端 `PageAggregateClientState` reducer 仍在,页面域单一真源未完全闭环。 -- 当前最优先的架构收口不是继续扩编辑器 UI,而是在 local-first MVP 基线上推进 `WorkspacePath/ObjectIdentity` runtime 消费统一、`DocumentBuffer/BufferStore`、`Page Aggregate` 单一真源收口、`tree command cutover` 收尾、`tree realtime event stream` live cache 统一(WS push 主链 2026-05-17 上线,SSE 降级为 fallback)和 agent diff / 冲突合并 / 本地索引 / 分享同步产品化。AI 侧普通 Markdown 编辑主路径已收口为“当前文件引用 + selection + allowed roots + agent 原生 patch/diff + watcher 同步”;`mnote.doc.fetch`、`mnote.doc.markdown_edit`、`mnote.doc.apply_block_ops`、`mnote.block.*` 只作为 cloud / remote agent / compat fallback 或复杂结构辅助;`review session`/流式 apply 属于 Phase C(设计冻结,当前不实施),不扩新 AI 功能,不把粗粒度 `mnote.page.save` 当成精确块编辑主入口。 +- 当前最优先的架构收口不是继续扩编辑器 UI,而是在 local-first MVP 基线上推进 `WorkspacePath/ObjectIdentity` runtime 消费统一、`DocumentBuffer/BufferStore`、`Page Aggregate` 单一真源收口、`tree command cutover` 收尾、`tree realtime event stream` live cache 统一(WS push 主链 2026-05-17 上线,SSE 降级为 fallback)和 agent diff / 冲突合并 / 本地轻量搜索 / 分享同步产品化。AI 侧普通 Markdown 编辑主路径已收口为“当前文件引用 + selection + allowed roots + agent 原生 patch/diff + watcher 同步”;知识库问答、资料 OCR / parse / index / RAG 主路径已收口到 LightRAG,不再维护 LiteParse / 本地 OCR / evidence 资料索引多套 provider;`mnote.doc.fetch`、`mnote.doc.markdown_edit`、`mnote.doc.apply_block_ops`、`mnote.block.*` 只作为 cloud / remote agent / compat fallback 或复杂结构辅助;`review session`/流式 apply 属于 Phase C(设计冻结,当前不实施),不扩新 AI 功能,不把粗粒度 `mnote.page.save` 当成精确块编辑主入口。 ## 组件定位 @@ -23,6 +23,7 @@ - `Mindmap` 是 `tree-first graph` 的一种视图和编辑挂件,不是对象真相层。 - `OnlyOffice` 是独立页面型编辑器,不直接嵌入 `BlockNote` 画布;正文中通常通过附件块跳转进入。 - `page-ai/block-edit-workflow` route 只保留为兼容门面或非 local source 的快捷路径,不作为 local-first 普通 Markdown 编辑默认主路径;local-first 下优先给 Hermes / Reasonix 授权文件引用并让 agent 使用自身文件编辑能力。`mnote.doc.markdown_edit` 是 cloud / remote agent / compat fallback;`mnote.block.*` 保留为结构性辅助。 +- `LightRAG` 是当前唯一默认知识库 RAG provider,负责资料库 OCR、parse、chunk、vector、graph、rerank、query 与跨文档问答。MNote 只维护 source registry、allowed roots / 权限、FileTree 灯号、资料库设置 UI、dashboard/status UI、citation/open-reference 映射和 agent tool facade;LiteParse、旧 OCR sidecar、旧 evidence search / local-index agent tools 已退役到历史 / recycle 边界。 - 树域三层模型:`Resource Tree`(kernel 对象组织真源)→ `File Tree`(主组织投影,`{title}.md` 为页面正文行)→ `Page Tree`(导航投影,不持有结构真相)。涉及资源归属优先走 Rust kernel 的 `KernelObjectIdentity` / `KernelProjectionResourceKind`。 ## Runtime 模块定位 @@ -70,6 +71,7 @@ - 涉及 local-first AI 普通 Markdown 编辑时,优先沿“授权文件引用 + `AiAccessScope` + allowed roots + agent 原生 patch/diff + 文件版本冲突模型 + watcher 同步”推进;涉及 cloud / remote agent / 复杂结构辅助时,才沿 `mnote.doc.*` / `mnote.block.*` Hermes tools 与 Rust `EditorCommand` 推进。`mnote.page.save` 只作为页面级兜底写入工具。 - 涉及资源归属(哪个页面拥有哪个 mindmap/附件)、object identity 和资源生命周期时,优先沿 Resource Tree → File Tree projection → `tree.resource.*` 命令面推进;`tree.*` 是正式命令面,`documents.*` 只视为兼容层。 - 页面 AI 编辑当前 local-first 主路径为:页面定位到真实 `.md` 文件,MNote 计算 `AiAccessScope` / allowed roots / selection,Hermes 或 Reasonix 在白名单目录内用自身 patch/diff/文件编辑能力写入,MNote 通过 watcher / refresh 同步 tiptap。`mnote.doc.markdown_edit` 只作为 cloud / remote agent / compat fallback;`mnote.block.*` 保留为结构性辅助(拖拽排序等)。两层操作模型已获 CLI Main(Lark Doc)参考实现验证。流式 apply + suggest/review(参考 BlockNote AI)作为 Phase C 设计冻结,当前不实施。 +- 涉及知识库资料索引 / OCR / RAG 时,默认沿 `mnote.knowledge_rag.*` / `/api/knowledge-rag/*` 和 LightRAG source registry 推进。图片源不能直接假设会被 LightRAG `/documents/scan` 识别,当前通过 MNote 生成 Markdown wrapper 后进入 LightRAG;LightRAG 后台完成状态通过 MNote 有界 status bridge/backoff 同步到 registry 与 FileTree 灯号,而不是新增无界轮询。 - 需要架构判断时,优先参考: - `/mnt/Data1T/mnote/ARCHITECTURE.md` - `/mnt/Data1T/mnote/CURRENT_ARCHITECTURE.md` @@ -89,8 +91,9 @@ - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-24-resource-tree-filetree-pagetree-source-contract-checklist-v1.md` - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-12-main-editor-object-tab-resource-alignment-checklist-v1.md` - `/mnt/Data1T/mnote/design/10-review/done/16-mnote-web-runtime-module-maintainability-checklist-v1.md` - - `/mnt/Data1T/mnote/design/03-rust-web/process/3-23-sidebar-local-folder-resource-runtime-followup-v1.md` + - `/mnt/Data1T/mnote/design/03-rust-web/reference/3-23-sidebar-local-folder-resource-runtime-followup-v1.md` - `/mnt/Data1T/mnote/design/07-ai/process/7-38-page-ai-sidebar-runtime-owner-split-v1.md` + - `/mnt/Data1T/mnote/design/07-ai/done/7-50-lightrag-knowledge-rag-provider-v1.md` ## 设计稿目录规则 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 9eec8e98..93044235 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,12 +1,14 @@ # MNOTE 当前架构梳理 -> 更新时间:2026-05-26 +> 更新时间:2026-06-07 > > 2026-05-22 口径更新:产品形态已切换为 local-first workspace,且初步 MVP 已建立;本地文件夹是默认数据真相,Rust SQLite control-plane 承接 auth、membership、share grants、sync state、AI policy、ACP/Hermes runtime session 等默认控制面。Convex 仅保留为历史迁移源、显式 cloud source / compat / sync replica 边界;根 `convex/` functions 源码已软删除到 `recycle/20260522-convex-runtime-retirement/convex/`,不再作为 active deploy source。相关设计已完成并迁入 `design/02-convex-rust-long-term-architecture/done/2-2-local-first-workspace-convex-control-plane-v1.md` 与 `design/02-convex-rust-long-term-architecture/done/2-8-convex-replace-with-rust-sqlite-control-plane-v1.md`。本地 Markdown 图片与附件上传已新增 `/api/local-folder/assets/upload`,页面内上传默认写入 `{mdBase}.assets/` 并保存相对 Markdown 路径,不再走 Convex media asset。 > -> **当前阶段:local-first MVP 后阶段。** 3000 下文档页、Sidebar/File Tree/Page Tree 三层树模型、tree command(`tree.*` preferred)、tree realtime WS 主链 / SSE fallback、本地 Markdown 上传、上传下载、AI 会话本地化与 VSCode-like agent 运行口径均已建立。剩余工作集中在 `WorkspacePath/ObjectIdentity` runtime 消费统一、`DocumentBuffer/BufferStore`、Page Aggregate compat 瘦身、tree command context、live cache 统一、VSCode-like 冲突合并 UI、agent diff 审计、本地索引 / 分享 / 同步闭环,而不是继续扩张专用 page-ai fast-path 或 Convex 主存储链。 +> **当前阶段:local-first MVP 后阶段。** 3000 下文档页、Sidebar/File Tree/Page Tree 三层树模型、tree command(`tree.*` preferred)、tree realtime WS 主链 / SSE fallback、本地 Markdown 上传、上传下载、AI 会话本地化与 VSCode-like agent 运行口径均已建立。剩余工作集中在 `WorkspacePath/ObjectIdentity` runtime 消费统一、`DocumentBuffer/BufferStore`、Page Aggregate compat 瘦身、tree command context、live cache 统一、VSCode-like 冲突合并 UI、agent diff 审计、本地轻量搜索 / 分享 / 同步闭环,而不是继续扩张专用 page-ai fast-path 或 Convex 主存储链。 > > 2026-05-26 口径更新:前端 runtime 大块已完成模块级拆分。`layout.rs` / `tree.rs` / `web_shell.rs` 不再是浏览器 JS 功能的主要符号定位入口;Sidebar、FileTree、document adapter、tree shell 等 runtime 入口位于 `rust/crates/mnote-web/browser/*.js`,tiptap island 入口位于 `rust/spikes/leptos-tiptap-spike/src/editor_runtime/*.rs`。CodeGraph 可直接索引这些 JS / Rust 模块;当前 smoke 分类见 `scripts/TESTING_REFERENCE.md`。 +> +> 2026-06-07 口径更新:知识库问答、资料 OCR / parse / index / RAG 已收口到 LightRAG 单外挂 provider。LiteParse、旧本地 OCR sidecar、旧 evidence search / local-index agent tools 不再作为默认 fallback;MNote 负责 source registry、权限、FileTree 灯号、资料库设置 UI、dashboard/status UI、citation/open-reference 映射和 agent facade。图片源通过 MNote 生成 Markdown wrapper 入 LightRAG;LightRAG 后台完成状态通过有界 status bridge/backoff 回写 MNote registry。当前 LightRAG 文本 / extract / query / VLM 模型口径为 `aigc/qwen-3.5`。 本文只描述当前仓库中真实成立的主线结构,以及当前最优先的架构收口点。 @@ -26,6 +28,7 @@ 4. 文档页默认主编辑器已切到页面内 `leptos-tiptap` island 5. `BlockNote` 已退出文档页默认主路径,仅作为历史参考实现 / 对照材料保留 6. 页面 AI 当前最合理的长期形态是:MNote 只负责页面定位、白名单目录权限、agent runtime 管理、文件变更同步;Hermes / Reasonix 直接在授权工作区内编辑本地文件。`page_ai_workflow` 只保留为 debug / 历史兼容门面,`mnote.doc.markdown_edit` 不作为 local-first 默认正文编辑入口,只在显式 cloud / remote / compat 且无法直接授权文件访问的边界中作为受控 fallback;`mnote.block.*` 只保留为复杂结构辅助层 +7. 知识库资料处理主线是 LightRAG:OCR、parse、chunk、向量、图谱、RAG query 和跨文档召回由 LightRAG 承担;MNote 不再并行维护 LiteParse / evidence SQLite / OCR sidecar 作为资料库索引真相 一句话收口: @@ -35,7 +38,7 @@ > **主 Web 执行面当前以 `mnote-web` 为 3000 owner;Next App Router 只保留为 legacy compat、交互 island bundle source 与显式 debug/迁移辅助边界。** > -> **页面 AI 的 local-first 主路径应尽量贴近 VSCode:当前页面解析成真实 `.md` 文件,Hermes / Reasonix 在授权目录白名单内直接读写,tiptap 只消费后台文件变化后的最新投影。** +> **页面 AI 的 local-first 主路径应尽量贴近 VSCode:当前页面解析成真实 `.md` 文件,Hermes / Reasonix 在授权目录白名单内直接读写,tiptap 只消费后台文件变化后的最新投影。知识库问答另走 LightRAG provider,经 MNote source registry 和 citation/open-reference 回到本地资源。** ## 2. 当前主线目录 @@ -93,7 +96,7 @@ - `rust/crates/mnote-web/browser/tree-shell-runtime.js`:debug/internal tree shell entrypoint,子模块负责 page tree、filetree、picker、render、DOM、icons 和 state。 - `rust/spikes/leptos-tiptap-spike/src/editor_runtime/`:tiptap island runtime 模块集合;根 `lib.rs` 只保留 wasm entrypoint、Leptos shell 信号编排和仍未稳定外置的薄壳。 -对应完成记录见 `design/10-review/done/16-mnote-web-runtime-module-maintainability-checklist-v1.md`。剩余专项边界不再回到 raw string 问题,而是分别由 `design/03-rust-web/process/3-23-sidebar-local-folder-resource-runtime-followup-v1.md` 和 `design/07-ai/process/7-38-page-ai-sidebar-runtime-owner-split-v1.md` 承接。 +对应完成记录见 `design/10-review/done/16-mnote-web-runtime-module-maintainability-checklist-v1.md`。剩余专项边界不再回到 raw string 问题,而是分别由 `design/03-rust-web/reference/3-23-sidebar-local-folder-resource-runtime-followup-v1.md` 和 `design/07-ai/process/7-38-page-ai-sidebar-runtime-owner-split-v1.md` 承接。 ## 4. 文档页主链 @@ -353,7 +356,27 @@ OnlyOffice 仍然是: - `/mnt/Data1T/mnote/design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md` - `/mnt/Data1T/mnote/design/old/07-ai/process/7-14-local-first-ai-markdown-editing-convergence-v1.md`(历史回收稿;不再作为当前实现依据) -### 8.5 定向 Bug Hunt 与质量基线 +### 8.5 Knowledge RAG / LightRAG + +目标: + +- 把资料库 OCR、解析、索引、图谱、召回和跨文档问答统一交给 LightRAG。 +- MNote 只保留 source registry、allowed roots / 权限、FileTree 灯号、资料库设置 UI、LightRAG dashboard/status UI、citation/open-reference 映射和 agent tool facade。 +- LiteParse、旧 OCR sidecar、旧 evidence search / local-index agent tools 作为历史迁移和 recycle 边界,不再作为默认 fallback。 + +当前进度(2026-06-07): + +- ✅ LightRAG 是当前唯一默认知识库 RAG provider;当前模型口径为 `aigc/qwen-3.5`。 +- ✅ MNote `source registry` 记录 `workspaceId/rootUri/sourcePath/sourceHash/lightRagDocId/stale/deletedAt/status`,FileTree 灯号从 registry 映射为已索引 / 正在索引 / 失败 / 未索引。 +- ✅ 图片源通过 Markdown wrapper 入 LightRAG,避免 LightRAG `/documents/scan` 忽略原始图片文件。 +- ✅ MNote 在 ingest / reindex 后通过有界 status bridge/backoff 对照 LightRAG `/documents`,把后台完成状态同步回 registry、设置面板和 FileTree。 +- ✅ Agent 默认使用 `mnote.knowledge_rag.status/query/open_reference`,不再暴露旧 `mnote.evidence.*` 作为资料库检索主路径。 + +对应设计稿: + +- `/mnt/Data1T/mnote/design/07-ai/done/7-50-lightrag-knowledge-rag-provider-v1.md` + +### 8.6 定向 Bug Hunt 与质量基线 目标: @@ -393,4 +416,4 @@ OnlyOffice 仍然是: 当前真正的卡点已经从“能不能跑”变成: -> **管理员目录授权、文件版本冲突合并、agent diff 审计、本地索引 / 分享 / 同步如何产品化;Page Aggregate ClientState、projection / command / realtime 兼容链如何继续瘦身。** +> **管理员目录授权、文件版本冲突合并、agent diff 审计、本地轻量搜索 / 分享 / 同步如何产品化;知识库 RAG 已交给 LightRAG;Page Aggregate ClientState、projection / command / realtime 兼容链如何继续瘦身。** diff --git a/CURRENT_ARCHITECTURE.md b/CURRENT_ARCHITECTURE.md index e1681af5..1a9abd9f 100644 --- a/CURRENT_ARCHITECTURE.md +++ b/CURRENT_ARCHITECTURE.md @@ -1,6 +1,6 @@ # 当前完整架构 -> 更新时间:2026-05-26 +> 更新时间:2026-06-07 > > 范围:`/mnt/Data1T/mnote` 当前可见实现、local-first MVP 后阶段主线口径、历史退役边界和后续功能缺口。 @@ -18,6 +18,7 @@ - Rust kernel / projection / command 持有树、页面、资源和权限语义。 - tiptap 是 Markdown 的前端显示与交互层,不是 agent 的主工作面。 - Hermes / Reasonix 默认应像 VSCode 中的 agent 一样,在授权目录白名单内用自身 patch / diff / 文件编辑能力修改文件。 +- LightRAG 是当前默认知识库 RAG provider,负责资料库 OCR / parse / index / RAG;MNote 负责 source registry、权限、FileTree 灯号、dashboard/status UI 和 citation/open-reference 映射。 - Rust SQLite control-plane 是默认 auth、membership、share grants、sync state、AI policy、ACP/Hermes runtime session 控制面;Convex / 服务端不再是默认正文、附件、AI 会话全文主存储,也不再是默认控制面,只保留历史迁移源、显式 cloud source、compat 和 sync replica 边界。 上位设计已完成并迁入: @@ -64,6 +65,16 @@ - `/api/page-ai/block-edit-workflow` 不再作为 local-first 普通正文编辑主路径。 - AI session 全文在 local source 下默认写入 `ai-sessions/private/*.jsonl` 或 `ai-sessions/shared//*.jsonl`;ACP/Hermes runtime session 默认落到 SQLite control-plane;Convex 只作为显式 legacy/cloud metadata / audit / sync replica 边界。 +### 2.5 Knowledge RAG / LightRAG + +- 知识库问答、资料 OCR、parse、chunk、vector、graph、rerank、query 统一由 LightRAG 承担;当前模型口径为 `aigc/qwen-3.5`。 +- MNote 不复制 LightRAG 图谱或 chunk,不把 LightRAG storage 当用户正文真相;原始 source 仍属于 local-folder / Resource Tree。 +- MNote 维护 `/.mnote/index/lightrag-source-registry.json`,用于映射 `workspaceId/rootUri/sourcePath/sourceHash/lightRagDocId/stale/deletedAt/status`。 +- FileTree 的资料库灯号来自 LightRAG source registry:绿灯已索引、黄灯正在索引或删除提交中、红灯失败 / 过期、无灯未索引或已移除。 +- 图片源先由 MNote 生成 Markdown wrapper,再交给 LightRAG scan;不能假设 LightRAG 会直接索引原始 `png/jpg`。 +- MNote 在 ingest / reindex 后用有界 status bridge/backoff 同步 LightRAG 后台完成状态,刷新 registry、资料库面板和 FileTree。 +- Agent 只通过 `mnote.knowledge_rag.status/query/open_reference` 访问知识库;旧 LiteParse、OCR sidecar、`mnote.evidence.*` 与本地资料索引工具已退役为历史 / recycle 边界。 + ## 3. 已完成 MVP 收口 - Local-first workspace 上位设计和 checklist 已完成,迁入 `design/02-convex-rust-long-term-architecture/done/2-2-*`。 @@ -98,7 +109,7 @@ - 减少 polling 或重复补偿链。 6. **Runtime owner 剩余专项** - - `design/03-rust-web/process/3-23-sidebar-local-folder-resource-runtime-followup-v1.md` 继续承接 local folder / resource 编排中仍适合拆出的稳定 helper。 + - `design/03-rust-web/reference/3-23-sidebar-local-folder-resource-runtime-followup-v1.md` 记录 local folder / resource 编排拆分后的参考边界;新的执行项应另起 process 稿。 - `design/07-ai/process/7-38-page-ai-sidebar-runtime-owner-split-v1.md` 继续承接 Page AI sidebar runtime owner 细化。 7. **管理员目录授权 UI / API** @@ -113,9 +124,10 @@ - local-first agent 应回收 changed files、diff summary、tool/run id、actor、workspace root、permission level。 - audit 默认本地落盘,同步开启时再上报控制面。 -10. **本地全文搜索、引用和索引** +10. **本地轻量搜索与知识库 RAG 边界** - 本地 `.md` workspace 需要独立索引:全文搜索、反链、页面引用、资源引用、标签。 - 不能依赖 Convex search 才能搜索本地工作区。 + - 多资料 OCR / parse / index / RAG 不再由本地 evidence / LiteParse 承担,默认走 LightRAG provider。 11. **分享与同步闭环** - 需要把 share grants、shared workspace cache、shared AI session、只读/可写权限和冲突处理连成产品级闭环。 @@ -142,5 +154,7 @@ - “`/api/documents/save` 是长期正文保存主入口。” - “BlockNote 是当前默认编辑器或系统事实源。” - “Next App Router / wolai-frontend 是 3000 主运行时。” +- “LiteParse / 本地 OCR sidecar / evidence search 仍是资料库 RAG 默认 fallback。” +- “MNote 需要为 LightRAG 结果再建一套 RAG 索引真相。” 历史文件如果需要保留这些说法,必须明确标注为 `[recycle]`、legacy、review snapshot 或 compat / cloud source 背景。 diff --git a/REASONIX.md b/REASONIX.md index 63cd354b..7de644ca 100644 --- a/REASONIX.md +++ b/REASONIX.md @@ -9,6 +9,7 @@ - Rust SQLite control-plane 已承接默认 auth、membership、share grants、sync state、AI policy、ACP/Hermes runtime session 等控制面;Convex / 服务端不再是默认正文、附件、AI 会话全文主存储,只保留历史迁移源、显式 cloud source、compat 和 sync replica 边界。 - `mnote-web` 是当前 3000 owner;旧 Next / React / wolai-frontend 已移入 `recycle/`,默认不作为当前实现依据。 - 前端 runtime 已完成第一轮模块级拆分;查浏览器 JS 功能默认先看 `rust/crates/mnote-web/browser/*.js`,查 tiptap island 行为默认先看 `rust/spikes/leptos-tiptap-spike/src/editor_runtime/*.rs`。不要把 `layout.rs` / `tree.rs` / `web_shell.rs` 的历史 raw string 当作当前符号定位入口。 +- Knowledge RAG 当前统一到 LightRAG:LiteParse、旧 OCR sidecar、旧 evidence search / local-index agent tools 不再作为默认 fallback;LightRAG 负责 OCR / parse / index / RAG,MNote 负责 source registry、权限、FileTree 灯号、资料库 UI、dashboard/status 和 citation/open-reference。当前模型口径为 `aigc/qwen-3.5`。 ## Stack @@ -24,6 +25,8 @@ |------|----------| | `rust/crates/mnote-web/` | Rust web gateway (axum + Leptos SSR, `:3000`) | | `rust/crates/mnote-web/browser/` | Browser runtime modules for sidebar, filetree, document adapter, tree shell, Page AI panel host | +| `rust/crates/mnote-web/src/routes/knowledge_rag.rs` | MNote LightRAG connector routes: status, ingest, query, delete, prune, open-reference | +| `rust/crates/mnote-web/src/hermes_tools/knowledge_rag.rs` | Hermes / Reasonix-facing knowledge RAG tool facade | | `rust/crates/core-protocol/` | Kernel types, projection protocol, tree/graph terms | | `rust/crates/bridge-runtime/` | Kernel query/command, projection bridge, source normalization | | `rust/spikes/leptos-tiptap-spike/src/editor_runtime/` | Leptos-tiptap island runtime modules; use before editing root `lib.rs` | @@ -80,6 +83,7 @@ - **Rust workspace**: edition 2021, resolver "2", MIT license. All crates inherit workspace version (`rust/Cargo.toml:2-8`). - **Convex boundary**: root `convex/` has been retired to `recycle/20260522-convex-runtime-retirement/convex/`; `infra/convex/` has been retired to `recycle/20260522-convex-runtime-retirement/infra/convex/`; do not recreate either as an active deploy source without a new architecture decision. `recycle/wolai-frontend/convex` is also historical. Keep Rust `transport/convex.rs` and `storage-convex-bridge` as explicit cloud/compat boundaries; do not mechanically delete them with the retired functions source. - **AI editing path**: local-first Markdown editing should prefer `currentFile + selection + allowedRoots / aiAccessScope + agent native patch/diff + watcher sync`. `mnote.doc.*` / `mnote.block.*` are cloud, remote, compat, or complex-structure helpers. +- **Knowledge RAG path**: use `mnote.knowledge_rag.status/query/open_reference` and `/api/knowledge-rag/*`. Do not use `mnote.evidence.*`, LiteParse, or local OCR sidecar as the default retrieval/indexing path. Image sources enter LightRAG through a Markdown wrapper; MNote status bridge/backoff syncs LightRAG background completion back to the registry and FileTree. - **Bug tracking**: bugs organized by design domain directory (e.g. `bugs/04-tree-domain/process/`), moved to `done/` when fixed (`bugs/README.md`). - **Design docs**: organized by domain in `design/`, with `process/` (in-progress) and `done/` (completed) subdirs (`design/README.md`). @@ -89,6 +93,7 @@ - **`.aionrs/`, `.claw/`, `.codex/`, `.gemini/`** — session data from other AI tools, not project code. Don't read or edit. - **`recycle/`** — retired code, not current implementation. Don't use as evidence for current behavior. - **Convex availability**: local-folder / Rust projection smoke should not require Convex. Auth, share grants, ACP/Hermes runtime state, and AI policy default to SQLite control-plane; Convex is required only for tests that explicitly cover legacy cloud source, compat, sync replica, migration, or rollback behavior. -- **MVP 后阶段优先级**: prefer WorkspacePath/ObjectIdentity runtime consumption, DocumentBuffer/BufferStore, Page Aggregate compat slimming, tree command context/context key, live cache unification, agent diff audit, conflict merge, local index, and share/sync productization. +- **LightRAG boundary**: LightRAG storage is derived cache, not user source truth. MNote registry is the mapping authority for source path, doc id, stale/deleted state, FileTree lights, and citation routing. +- **MVP 后阶段优先级**: prefer WorkspacePath/ObjectIdentity runtime consumption, DocumentBuffer/BufferStore, Page Aggregate compat slimming, tree command context/context key, live cache unification, agent diff audit, conflict merge, local lightweight search, and share/sync productization. Knowledge RAG belongs to LightRAG, not a revived local evidence/LiteParse index. - **Rust wasm32 target** required (`rust/rust-toolchain.toml`), needed for `tree-shell-runtime-wasm` crate. - **`rust/target/`** is gitignored and large — `cargo build` runs from scratch if cache is missing. diff --git a/design/07-ai/done/7-50-lightrag-knowledge-rag-provider-v1.md b/design/07-ai/done/7-50-lightrag-knowledge-rag-provider-v1.md new file mode 100644 index 00000000..0c0af1f3 --- /dev/null +++ b/design/07-ai/done/7-50-lightrag-knowledge-rag-provider-v1.md @@ -0,0 +1,757 @@ +# 7-50 [done] LightRAG Knowledge RAG Provider v1 + +> 创建时间:2026-06-06 +> +> 当前状态:`DONE` +> +> Owner:07-ai / knowledge-rag / plugin-ui / external-provider +> +> 取代:`design/old/07-ai/process/7-49-local-understanding-graphrag-kernel-v1.md` +> +> 参考代码: +> - `reference-code/LightRAG` +> +> 本机源码部署: +> - source:`/mnt/Data1T/Mnote_data/lightrag/LightRAG` +> - storage:`/mnt/Data1T/Mnote_data/lightrag/rag_storage` +> - input spike:`/mnt/Data1T/Mnote_data/lightrag/inputs` +> - systemd user service:`mnote-lightrag.service` +> - WebUI:`http://127.0.0.1:9621` +> - current model:`aigc/qwen-3.5` +> +> 上位依据: +> - `/mnt/Data1T/mnote/ARCHITECTURE.md` +> - `/mnt/Data1T/mnote/CURRENT_ARCHITECTURE.md` +> - `design/07-ai/process/7-46-document-evidence-retrieval-kernel-v1.md` +> - `design/07-ai/process/7-48-paperless-ngx-reference-resource-ingestion-job-index-v1.md` +> - `design/03-rust-web/done/3-25-local-folder-mineru-ocr-sidecar-v1.md` + +## 1. 第一结论 + +MNote 后续知识库问答主线改为 **LightRAG 单外挂 Provider**,不继续推进 7-49 的自研 Understanding GraphRAG Kernel / mindmap projection 路线。 + +目标是减少冗余项目和冲突: + +- LightRAG 负责知识库问答相关的 OCR、文档解析调度、MinerU / Docling 接入、sidecar、chunk、向量、图谱、rerank 和跨文档问答。 +- MNote 负责 workspace 权限、source truth、resource ownership、引用 UI、agent 授权、任务入口和 LightRAG 结果打开方式。 +- LiteParse / MNote 旧解析定位链路退役;旧 OCR / evidence / 本地资料索引入口不再作为默认 fallback,PDF / Office / 图片 OCR 与资料库索引统一交给 LightRAG。 +- PageIndex 暂停,不作为当前默认双引擎;未来只在少量书籍确实需要章节树精读时再做定向树索引。 +- 插件系统暂不开发完整市场式框架;第一版只做一个薄的 `KnowledgeRagProvider` / `LightRAGAdapter`,让 LightRAG 像插件一样不干扰 MNote 正常功能。 + +## 2. 为什么放弃 7-49 + +7-49 的核心假设是 MNote 自己构建本地 Understanding GraphRAG Kernel。这个方向问题不在能力,而在冗余: + +- LightRAG 已经覆盖 graph + vector + rerank + parser pipeline 的主体能力。 +- 自研 understanding graph 会和 LightRAG 图谱、evidence SQLite、mindmap projection 形成三套相似真相。 +- mindmap projection 不能覆盖 NotebookLM 式多书问答的核心体验,反而会牵引 UI 和数据模型提前复杂化。 +- 用户当前需求是“很多本书 / 很多篇文献作为资源,agent 能查阅、问答、溯源”,优先级高于自研图谱可视化。 + +因此 7-49 退役,只保留为历史参考,不再作为 process 设计稿。 + +## 3. LightRAG 能覆盖的边界 + +本轮已按最新 LightRAG 代码重新建索引确认: + +- LightRAG v1.5 已合并 RagAnything 路线,支持 MinerU / Docling 多模态解析。 +- 支持 `legacy` / `native` / `mineru` / `docling` 解析引擎。 +- 支持 Fix / Recursive / Vector / Paragraph 四种切分。 +- MinerU 支持 `official` / `local` 两种模式;MNote 已有 MinerU token,可先走 official spike。 +- sidecar 包含 `blocks.jsonl`、`drawings.json`、`tables.json`、`equations.json`、assets。 +- block positions 支持 `bbox`,可表达 page + rectangle。 +- chunk 可以通过 `sidecar.refs` 追溯到 block/table/drawing/equation。 + +关键限制: + +- LightRAG 没有集成 LiteParse 本身。 +- LightRAG OCR 能力来自 MinerU / Docling 服务,不是内置 OCR 模型。 +- LightRAG `/query` 默认引用粒度偏粗,通常是 `reference_id + file_path + chunk content`;MNote 需要额外 adapter 反查 sidecar,生成现有 EvidenceLocator。 + +结论:LightRAG 可以成为主解析/RAG provider,但不能直接替代 MNote 的权限、locator 和引用 UI。 + +## 4. 新架构边界 + +本设计的核心边界是:**LightRAG 是独立本地知识库底座服务,MNote 是 UI + source manager + permission layer**。 + +这不是把 LightRAG 深度嵌入 MNote,也不是让 MNote 复制 LightRAG 内部索引。LightRAG 可以直接对 MNote 暴露的本地文件夹做 RAG / OCR;MNote 只提供允许访问的目录、用户操作入口、任务控制、结果展示和引用打开。 + +```text +用户资料库 + PDF / EPUB / DOCX / PPTX / Markdown / 图片 + | + v +MNote LightRAG Connector + - allowed roots + - workspaceId / rootUri + - source file identity + - task control UI + - reference open action + | + v +LightRAG Service / Dashboard + - MinerU / Docling parser + - sidecar + - chunk / vector / graph / rerank + - query / query_data + - task state + | + v +MNote Answer Renderer + - answer + - citations + - LightRAG reference + - MNote open action + - locator fallback +``` + +MNote 不复制 LightRAG 的图谱,不把 LightRAG 的 chunk 当正文真相。LightRAG 是派生知识库;原始文件仍属于 MNote local-folder / resource tree。 + +### 4.1 OCR / 索引归属 + +知识库问答相关的 OCR、parse、chunk、vector、graph、rerank、query index 全部归 LightRAG。 + +MNote 不再为“问一堆书 / 问一堆论文”维护第二套 OCR / parse / chunk / vector / graph。此前 MNote 为 OCR、索引、source-map、evidence graph 维护过多套派生链路,已经制造了过多 bug 和职责冲突;7-50 的目标就是把这部分降为一个外部 provider。 + +MNote 可以保留轻量 UI 索引,但它只服务 MNote 正常功能: + +- 页面标题、Markdown 正文快速搜索。 +- tag / backlink / 最近页面。 +- 文件树和资源元数据。 +- 当前编辑器内搜索。 + +区分标准: + +| 场景 | Owner | +| --- | --- | +| 搜我的笔记页面、标签、最近编辑、backlink | MNote 轻索引 | +| 问多本书 / 多篇论文 / PDF / 扫描件 / Office 附件 | LightRAG | +| 解析 PDF / OCR 图片 / 建向量 / 建图谱 / rerank | LightRAG | +| 点击来源、打开 MNote resource tab、显示降级来源 | MNote connector | + +### 4.2 插件式不干扰原则 + +LightRAG 第一阶段必须像无害插件: + +- 不修改用户 Markdown 正文。 +- 不写 MNote 文件树中的业务文件。 +- 不接管 MNote 普通搜索。 +- 不让 MNote UI 依赖 LightRAG 才能打开页面。 +- 不在 MNote active code 中复制 LightRAG pipeline。 +- 不把 LightRAG parser/cache/index 失败扩散为 MNote 文件树或编辑器故障。 + +MNote 和 LightRAG 的连接面应尽量小: + +- 配置 LightRAG endpoint / working dir / token 注入方式。 +- 给 LightRAG 一组 allowed roots 或 source list。 +- 触发 ingest / reindex / delete source。 +- 查询 LightRAG result。 +- 把 LightRAG reference 映射为 MNote open action。 +- 在 MNote 中嵌入或打开 LightRAG dashboard,用于任务控制和 graph 查看。 + +### 4.3 Source Truth / 删除语义 + +7-50 的基线不采用“上传一份副本到 LightRAG staging 目录再索引”的默认模式。这个模式会制造两个问题: + +- 反复真相:MNote 本地文件是一份,LightRAG input/staging 又是一份,用户编辑、移动、删除后两边容易不一致。 +- 删除源后仍可检索:LightRAG 索引是派生缓存,如果没有收到 source delete / stale 标记,旧 chunk / vector / graph 仍可能继续回答。 + +因此 7-50 的 source truth 规则是: + +- 原始资料真相永远是 MNote local-folder / resource tree 中的 host absolute path。 +- LightRAG 的 storage、sidecar、chunk、vector、graph 都是派生缓存。 +- MNote 默认不把用户资料复制到 LightRAG input 目录;除非 LightRAG API 当前阶段只能 upload,才允许使用临时受控 staging,并且必须记录为 spike 降级方案。 +- LightRAG connector 必须维护 source registry: + - `workspaceId` + - `rootUri` + - `sourcePath` + - `sourceHash` + - `lightRagDocId` + - `indexedAt` + - `deletedAt` + - `stale` +- source 文件删除、移动、重命名或 hash 变化后,MNote watcher 必须把 registry 标记为 stale,并触发 LightRAG delete / reindex。 +- 删除 source 后,query scope 必须过滤 stale / deleted source;如果 LightRAG 侧删除尚未完成,MNote 层不得把该 source 的 citation 当成有效来源展示。 +- LightRAG 官方 `DELETE /documents/delete_document` 能删除文档状态、chunks、vector、graph;`delete_file=true` 还会删除 input 目录文件。MNote 集成默认只删除 LightRAG 派生索引,不删除用户原始文件。 +- 改 parser、embedding dim、storage backend、source parser hint 等影响索引一致性的配置时,必须走“delete old doc -> reindex source”,不能隐式覆盖旧索引。 + +这个边界决定部署方式:Docker 可以用于隔离 demo,但不作为 7-50 与 MNote connector 的基线。7-50 基线采用源码 / host 部署,保证 LightRAG 能看到 MNote 的真实本地路径、watcher 能做删除同步,MNote adapter 也能以同一套路径身份做引用打开。 + +### 4.4 Source Truth Spike 结果 + +2026-06-06 已完成一个受控 source-truth spike: + +- MNote source 文件:`/mnt/Data1T/Mnote_data/lightrag/source-spike/mnote-source-truth-spike.txt` +- LightRAG input 入口:`/mnt/Data1T/Mnote_data/lightrag/inputs/mnote-source-truth-spike.txt`,以 symlink 指向 source 文件。 +- 触发方式:`POST /documents/scan` +- LightRAG doc id:`doc-0092d6976b80946591c9d129e92d989a` +- 处理结果:`processed`,`chunks_count=1`,`parse_engine=legacy`,`file_path=mnote-source-truth-spike.txt` +- `query` 能回答 `CARBOXY_PROTECTOR_ALPHA_7349`,并返回 reference。 +- `query/data` 能返回 entities / relationships / chunks / references,其中 chunk 带 `chunk_id` 与 `file_path`。 +- `DELETE /documents/delete_document` + `delete_file=false` 删除后,documents 为空,`query/data` 返回 `no_results`,`query` 返回 no-context 且 references 为空。 +- 删除 LightRAG 派生索引没有删除 MNote source 文件;symlink 被 LightRAG scan 移入 `inputs/__parsed__/`。 + +结论: + +- symlink scan 可以避免复制 source 内容,但 LightRAG 内部 `file_path` 只保留 basename,不保留 host absolute path。 +- 因此 MNote connector 仍必须维护 source registry,把 `workspaceId/rootUri/sourcePath/sourceHash/lightRagDocId/indexedAt/deletedAt/stale` 映射到 LightRAG doc id 和 basename。 +- `delete_file=false` 是 MNote 删除 LightRAG 派生索引的默认语义;不能让 LightRAG 删除用户 source。 +- source delete / move / rename 的自动触发仍需要 MNote watcher + registry,不能仅依赖 LightRAG scan。 + +### 4.5 Ingestion Path Spike 结果 + +2026-06-06 继续补测 SDK / REST upload / input scan / symlink scan 四种入口: + +| 入口 | 测试方式 | 结果 | 对 MNote connector 的判断 | +| --- | --- | --- | --- | +| SDK `ainsert` | 独立临时 storage 调用 `LightRAG.ainsert(..., file_paths=/mnt/.../source.txt)` | `processed`,chunk token 命中;source 文件未被删除;`file_path` 仍被规整成 basename | 可作为 adapter 内部能力,但不能依赖 LightRAG 保存 host absolute path;仍要 registry | +| REST `/documents/text` | 调用 text API,`file_source` 传 host absolute path | `processed`,`process_options=F`;`file_path` 被规整成 basename | 适合纯文本 / Markdown 内容直入,但无法表达真实附件生命周期 | +| REST `/documents/upload` | multipart 上传一个文本文件 | `processed`,文件被放入 `inputs/__parsed__/`;`file_path` 为上传文件名 | 只适合作 fallback / UI 手动上传,不适合作 MNote 默认 source truth | +| `input_dir` real file + `/documents/scan` | 写入真实 input 文件后触发 scan | `processed`,scan 会把 input 文件移入 `__parsed__` | 可用于受控 staging,但会制造第二份 source,不作为默认 | +| symlink + `/documents/scan` | `inputs/*.txt` symlink 指向 MNote source 文件 | `processed`,symlink 被移入 `inputs/__parsed__/`,原始 source 保留;`file_path` 为 symlink basename | 当前最接近 MNote source-truth 基线;必须由 registry 管理 symlink basename 到 host source | + +额外观察: + +- `/documents/scan` 在 pipeline busy 时会返回 `scanning_skipped_pipeline_busy`,不会排队;MNote connector 必须做 job ledger / retry,而不能把一次 scan 失败视作 ingestion 完成。 +- SDK、text API、upload、scan 都不会保留 host absolute path 到 LightRAG `file_path`;LightRAG 引用只能作为 provider-local reference,MNote 侧必须用 `source registry` 反查。 +- 本轮临时 server storage 中产生的四个测试 doc 已用 `DELETE /documents/delete_document` + `delete_file=false` 删除;独立 SDK storage 留在 `/tmp/mnote-lightrag-sdk-*` 作为一次性验证产物。 + +### 4.6 MNote Connector First Cut + +2026-06-06 已落第一版 MNote connector,不引入完整插件市场: + +- 新增 `rust/crates/mnote-web/src/routes/knowledge_rag.rs`。 +- 新增 routes: + - `GET /api/knowledge-rag/status` + - `POST /api/knowledge-rag/ingest` + - `POST /api/knowledge-rag/query` + - `POST /api/knowledge-rag/open-reference` + - `POST /api/knowledge-rag/delete-source` +- 新增 source registry:`.mnote/index/lightrag-source-registry.json`。 +- `ingest` 默认创建 `input_dir` symlink 后触发 `/documents/scan`,不复制用户 source 内容。 +- `delete-source` 调用 LightRAG `DELETE /documents/delete_document` 时固定 `delete_file=false`,只删除派生索引,不删除用户 source。 +- `query` 只把 LightRAG raw result 作为 provider 结果返回;MNote 额外生成 `references`,用 registry 把 LightRAG `file_path` 映射回 MNote source。 +- `open-reference` 未命中 registry 或 source 已 stale/deleted 时返回 `locatorDegraded=true`,不伪造 page / bbox。 +- Hermes/Page AI manifest 新增: + - `mnote.knowledge_rag.status` + - `mnote.knowledge_rag.query` + - `mnote.knowledge_rag.open_reference` +- 新增 skill:`skills/mnote-knowledge-rag/SKILL.md`。 +- Reasonix ACP wrapper 新增对应工具:`mnote_knowledge_rag_status` / `mnote_knowledge_rag_query` / `mnote_knowledge_rag_open_reference`。 +- Route smoke 已通过: + - `GET /api/knowledge-rag/status` 返回 LightRAG `healthy` / `webui_available=true`。 + - `POST /api/knowledge-rag/ingest` 对 `knowledge-rag-route-probe.md` 创建 symlink 并触发 `scanning_started`。 + - registry 同步到 `lightRagDocId=doc-0b7eabd99e33100ed94fc929143bb632`,`indexedAtMs` 有值。 + - `POST /api/knowledge-rag/query` 返回 1 条 registry 命中的 reference,`locatorDegraded=false`。 + - `POST /api/knowledge-rag/delete-source` 返回 `deletion_started`,registry 标记 `stale=true/deletedAtMs`,原始 source 文件仍存在。 + - Hermes manifest 带 `mnote-knowledge-rag` capability pack 和三项 `mnote.knowledge_rag.*` tool。 + +2026-06-06 运行态复核发现:3000 dev runtime 继承了历史 `LIGHTRAG_API_KEY` 环境变量,导致 MNote connector 优先使用旧 key,请求当前 `mnote-lightrag.service` 返回 `403 Invalid API Key`。已修正 `lightrag_api_key()` 优先级为 `MNOTE_LIGHTRAG_API_KEY` -> LightRAG source `.env` -> 通用 `LIGHTRAG_API_KEY` 兜底,避免旧 LightRAG 环境污染 MNote connector。临时 3027 server 带 `LIGHTRAG_API_KEY=bad-legacy-key` 复测,status / query 均可通过 source `.env` 正确访问当前 LightRAG。 + +### 4.7 PDF / MinerU Sidecar 验证结果 + +2026-06-06 已用 `mnote-e2e` local workspace 通过 MNote connector 跑 1 本书籍 PDF、2 篇论文 PDF、1 个扫描 PDF: + +| 样本 | MNote source | LightRAG doc | 结果 | 观察 | +| --- | --- | --- | --- | --- | +| 书籍 PDF | `knowledge-rag-fixtures-7-50/book-q1-fy25-earnings.pdf` | `doc-32765c52c02777e6c8f8bc452ac0157e` | `processed` | MinerU,`chunks_count=38`,`mm_chunks=25` | +| 论文 PDF | `knowledge-rag-fixtures-7-50/paper-brotli-comparison.pdf` | `doc-06f4c43d18a0398beeb0f31e91b81466` | `processed` | MinerU,`chunks_count=6`,`mm_chunks=3` | +| 论文 PDF | `knowledge-rag-fixtures-7-50/paper-masterformat-partial.pdf` | `doc-633d81d17d26727bf600f54a7591657b` | `processed` | MinerU,`chunks_count=1` | +| 扫描 / 图片 PDF | `knowledge-rag-fixtures-7-50/scan-image-start.pdf` | `doc-14a5926e5c673f446d2be735259c622f` | `processed` | MinerU,`chunks_count=1` | + +补充观察: + +- PDF 不应强制 `parserHint=native`;LightRAG 当前 native hint 不支持 PDF,之前强制 native 的 PDF 已验证会失败。 +- 对同内容扫描 PDF 再入库时,LightRAG 会标记 duplicate,并通过 `metadata.original_doc_id` 指向原始 doc;MNote registry 当前只把 processed doc 当成可索引完成,duplicate failed 不应作为有效 citation。 +- Sidecar 样本实际落在 `/mnt/Data1T/Mnote_data/lightrag/inputs/__parsed__/*.{pdf}.parsed/*.blocks.jsonl` 与对应 `*.mineru_raw/`,不提交原文 fixture。 +- `blocks.jsonl` 的 content block 已包含 `positions: [{ type: "bbox", anchor: "1", range: [...] }]`,其中 `anchor` 可作为页码来源,`range` 可转为 bbox。 +- LightRAG `kv_store_text_chunks.json` 与 `/query/data` chunks 当前没有 `refs` 字段;chunk 只能拿到 `chunk_id`、`file_path`、`reference_id`、`content`。因此 MNote 先把 query reference enrich 出 `chunkId` / `quote`,再用 `file_path -> parsed sidecar -> blocks.jsonl -> positions` 反查 page/bbox。 +- 已新增第一版 `LightRAGSidecarLocator`:用 chunk content / chunk id fallback 匹配 sidecar block,生成 MNote `EvidenceLocator`;匹配失败时继续显示 locator degraded,不伪造页码。 + +当前已能把一部分 LightRAG PDF 引用升级成真正的 PDF page/bbox EvidenceLocator,但仍不是无损映射:因为 LightRAG chunk 缺少 `refs`,MNote 当前依赖 chunk quote 与 sidecar block 文本近似匹配。后续如果 LightRAG 暴露 chunk refs,应改为 refs 优先、文本匹配 fallback。 + +### 4.8 运行态复核结果 + +2026-06-06 运行态复核: + +- `mnote-lightrag.service` 为 systemd user service,当前 active running;入口为 `/mnt/Data1T/Mnote_data/lightrag/LightRAG/.venv/bin/lightrag-server`,配置来自 `/mnt/Data1T/Mnote_data/lightrag/LightRAG/.env`。 +- `GET /api/knowledge-rag/status` 返回 LightRAG `healthy`,`webui_available=true`,`dashboardUrl=http://127.0.0.1:9621`。 +- `GET http://127.0.0.1:9621/` 跳转 `/webui/`,`GET /webui/` 返回 200;但尚未做完整浏览器交互验证 dashboard 的 documents / graph / task 面板。 +- `scripts/task529-knowledge-rag-citation-resource-tab-smoke.js` 已通过:`POST /api/knowledge-rag/query` 能召回 `knowledge-rag-fixtures-7-50/scan-image-start.pdf`,citationUrl 打开本地 PDF resource tab,带 `page=1` / `bbox=171,126,648,152`,页面内 evidence 高亮渲染。 +- `POST /api/hermes/tools/mnote/call` 调用 `mnote.knowledge_rag.query` 已通过,工具层能返回 references 与 `citationMarkdown`;这验证的是 agent tool 检索通道,不等于 Page AI 真实最终回答质量已验收。 +- 召回已经可用,但排序还未验收:`scan image start` 查询能召回目标扫描 PDF,不过 top-1 可能是其他 PDF,后续需要专门评估 query mode / rerank / source scope。 +- MNote topbar 已新增独立“资料库问答”设置入口,不混入本地索引设置;该面板能读取 `/api/knowledge-rag/status`,展示 LightRAG healthy 状态、dashboard/input/storage、source registry,并提供相对路径 source 输入、ingest、刷新、打开 dashboard 动作。浏览器 smoke 已验证面板在 3000 打开后显示 `LightRAG healthy,已登记 5 个来源`。 +- `/api/knowledge-rag/ingest` 已支持 sourcePath 指向文件或目录;目录会在授权 root 内递归展开受支持文件,跳过 `.mnote` / `.git` / `node_modules` / `target` / `__parsed__` / `.venv`,并限制单次最多 200 个文件,避免误把整个工作区无限制灌入 LightRAG。对应单元测试已覆盖目录展开、内部目录跳过和不支持扩展名拒绝。 +- 资料库问答设置面板已支持从当前 filetree 选中项填入 sourcePath,并在 ingest 输入行展示 `待索引` / `索引中` / `已提交` / `需重试` / `失败` 状态。浏览器 smoke 已验证选中 `knowledge-rag-fixtures-7-50` 后点击“使用选中文件树项”能把该目录写入 source 输入框。 +- 运行态继续发现 `3000` dev runtime 继承了旧 Windows `INPUT_DIR=F:/SOFT/MNOTE/LightRAG/inputs` / `WORKING_DIR=F:/SOFT/MNOTE/LightRAG/data`,导致 sidecar locator 在当前 Linux LightRAG storage 中查不到 `blocks.jsonl`,`task529` 一度降级。已修正 connector 路径优先级为 `MNOTE_LIGHTRAG_*` -> LightRAG source `.env` -> 通用 legacy env -> 默认值,并补 `lightrag_paths_prefer_source_env_over_legacy_process_env` 单测。 +- `task529-knowledge-rag-citation-resource-tab-smoke.js` 复跑通过:`scan image start` 当前 top reference 为 `knowledge-rag-fixtures-7-50/scan-image-start.pdf`,`citationMarkdown=[scan-image-start.pdf · p.1](...)`,`citationUrl` 带 `resourceTab/page=1/bbox=171,126,648,152/blockId=f69c1af99063b608b4031a3c51864265`,浏览器资源标签页能渲染 PDF 和 evidence 高亮。 +- Agent 工具输出已改为 `mnote.knowledge_rag.agent_query_result.v1` 紧凑结构,优先给 `references/citations/citationMarkdown/locator`,不再把 LightRAG raw chunks 放在工具输出前面,避免 Page AI 最终回答看不到 citation 或把 raw retrieval 当回答。 +- `hybrid` mode 已在 MNote connector 中别名到 `mix`,避免 agent 复用本地 evidence search 的 `hybrid` 习惯时召回偏离;同时对 mapped references 做 query lexical rerank,让明确文件名 / 关键词的命中优先出现在 references 前列。`query_ranking_prefers_exact_source_and_quote_match` 与 `query_mode_aliases_hybrid_to_mix_for_lightrag` 已覆盖。 +- 新增 `scripts/task530-knowledge-rag-page-ai-final-answer-smoke.js`,真实浏览器打开 Page AI drawer,切到 Reasonix,发送资料库问题并验证最终可见回答包含 `scan-image-start.pdf · p.1` 的可点击 resourceTab citation,且不泄漏 raw JSON / 工具名 / 检索过程叙述。该 smoke 已通过。 +- 新增 `scripts/task531-lightrag-dashboard-ui-smoke.js`,从 MNote `/api/knowledge-rag/status` 读取 `dashboardUrl` 后打开 LightRAG WebUI,真实浏览器验证 Documents / Knowledge Graph / Retrieval 三个入口可见:Documents 显示 `Completed (5)` / `Fail (4)` / 扫描 PDF doc id 与 summary,Graph 显示 `Connected`,Retrieval 显示 `Query Mode` / `KG Top K` / `Connected`。截图落在 `tmp/task531-lightrag-dashboard-ui-smoke/`。 + +### 4.9 旧搜索 / Evidence 退役结果 + +LightRAG 引入后,旧搜索链路先完成回归确认,随后按最新产品决策退役 LiteParse / evidence fallback。当前分工是: + +- `/api/evidence/search` 已返回 `410 Gone` / `mnote_evidence_search_retired`,不再作为 agent fallback。 +- `mnote.evidence.search/read/open` 与 `mnote.index.status/refresh/update_settings` 不再暴露在 Hermes / Reasonix manifest 与 capability pack 中。 +- `/api/local-folder/ocr/jobs/status/read/insert/delete` 已返回 `410 Gone` / `mnote_local_ocr_retired`,旧 OCR sidecar API 不再作为 UI 或 agent 通道。 +- `mnote-local-index` skill 退役;兼容 alias `mnote-document-evidence` / `mnote-local-index` 映射到 `mnote-knowledge-rag`。 +- `/api/search/documents` 继续保留 MNote 页面 / Markdown resource 普通搜索,但 `includeOcr` 不再读取 `.mnote/ocr-index.json`;PDF / Office / 图片 OCR 与资料库问答走 LightRAG。 +- `/api/knowledge-rag/query` / `mnote.knowledge_rag.query` 是资料库问答和跨资料 citation 的默认路径。 +- FileTree `data-index-status` 已改为读取 LightRAG source registry,而不是 LiteParse sidecar / evidence.sqlite / local OCR index。 + +2026-06-06 已补跑旧链路 smoke,作为退役前基线;随后相关 active smoke 已软归档到 `recycle/20260606-liteparse-retirement/`: + +- `scripts/task528-document-evidence-liteparse-agent-smoke.js` 通过:直接 `/api/evidence/search` 命中 `Printer test page`;`mnote.evidence.search` 返回同一 PDF evidence 的 `quote/page/bbox/sourceMapPath`;Reasonix ACP 实际调用 `mnote_evidence_search`;LiteParse sidecar 与 `.mnote/index/evidence.sqlite` 均存在。 +- `scripts/task529-local-search-result-open-locator-smoke.js` 通过:`/api/search/documents` 搜索 `三甲基硅酯` 后,点击结果仍停留在当前文档页并打开 Markdown resource tab,定位到 `docs/Silicon.md` 命中段落,截图落在 `tmp/task529-local-search-result-open-locator-smoke/`。 +- `scripts/task452-local-search-index-browser-smoke.js` 已按当前独立索引设置 surface 更新并通过:显式保存 `includePaths=["."]` 后刷新索引,初次搜索能找到新建 Markdown;导航页和文档页都能打开独立索引面板,不回落到页面设置页签;`/api/search/local-index/backlinks` 与 `/api/search/local-index/tags` 仍返回 200;重命名后刷新索引,搜索结果更新到新路径且不再返回旧路径。 +- `scripts/task526-local-folder-ocr-api-smoke.js` 已从 active scripts 移到 recycle;Page AI raw resource target smoke 已改为确认不再携带 `ocrContext` / `ocrRootRelativePath`。 + +结论:旧 evidence 能力曾验证可用,但当前默认产品路径已切到 LightRAG;历史 sidecar 只作为归档数据解释材料,不再驱动新的 OCR / 索引 / agent 检索。 + +### 4.10 DOCX / Office Ingestion 验证结果 + +2026-06-06 新增 `scripts/task532-knowledge-rag-docx-ingestion-smoke.js`,用现有 OnlyOffice smoke DOCX 生成带唯一 marker 的 DOCX fixture,走 MNote `/api/knowledge-rag/ingest` 入库并真实查询: + +- DOCX source:`knowledge-rag-fixtures-7-50/docx-rag-smoke-1780745370830.docx` +- LightRAG doc id:`doc-2f561f74048372a3f66b7bbe8a6e713a` +- LightRAG file path:`mnote-4e26c0dd3d2bf575-docx-rag-smoke-1780745370830.docx` +- 查询 marker:`DOCX RAG SMOKE 1780745370830` +- 结果:LightRAG 能解析 DOCX 正文并召回包含 marker 的 chunk;MNote registry 能把 LightRAG file_path 映射回 workspace source path。 +- citation:DOCX 当前没有 PDF page/bbox sidecar locator,因此 `locatorDegraded=true`;但 connector 已补 fallback citation URL,把引用打开到同工作区 Markdown host 页并恢复 DOCX resource tab,浏览器验证 `panelKind=office`、`panelResourcePath=knowledge-rag-fixtures-7-50/docx-rag-smoke-1780745370830.docx`。 + +结论:DOCX ingestion、召回、source registry 映射和资源 tab 回跳已跑通;DOCX 仍没有页码/bbox 级定位。PPTX/XLSX 还未作为 7-50 验收样本补测。 + +### 4.11 Source Watcher / Stale Sync 验证结果 + +2026-06-06 已把 LightRAG source registry 同步接入 MNote 现有 local-folder watcher,而不是新增第二套 watcher: + +- `rust/crates/mnote-web/src/routes/local_folder_events.rs` 的 tree live watcher 收到 `watch_batch` 前,会调用 `knowledge_rag::sync_registry_for_root(...)`。 +- `knowledge_rag::sync_registry_with_documents(...)` 会同步 LightRAG `/documents` 状态,并额外检查 registry 中 source 的当前文件状态。 +- source 文件不存在时:registry 标记 `stale=true`、`deletedAtMs`、清空 `lightRagDocId/indexedAtMs`,并 best-effort 调用 LightRAG `DELETE /documents/delete_document`,固定 `delete_file=false`。 +- source hash 变化时:registry 标记 `stale=true`、清空旧 `lightRagDocId/indexedAtMs`、更新 `sourceHash`,并 best-effort 删除旧 LightRAG doc,避免旧 chunk 继续作为有效引用。 +- `mapped_references(...)` 过滤 stale / deleted registry 命中的 references;即使 LightRAG 删除尚未完成,MNote query 结果也不把该 source 当成有效 citation 返回。 + +新增验证: + +- `cargo test -p mnote-web knowledge_rag -- --nocapture` 通过 14 项;其中 `source_state_marks_deleted_and_changed_entries_stale` 覆盖删除 / hash 变化 registry 状态。 +- `scripts/task533-knowledge-rag-source-watcher-sync-smoke.js` 通过:真实 ingest `knowledge-rag-fixtures-7-50/watcher-stale-1780745838741.md`,打开 `/api/local-folder/events?treeLive=true`,删除 source 后收到 `Remove(File)` watch_batch;不调用 status 的情况下直接读 registry,确认 `stale=true`、`deletedAtMs`、`lightRagDocId=null`、`indexedAtMs=null`;随后 query marker 不再返回该 source 的有效 reference。 + +当前边界:delete / missing source 的 watcher 闭环已浏览器验证;hash 变化由单元测试覆盖;rename / move 本质上会让旧 source path missing,走同一 stale/delete 逻辑,但尚未单独补浏览器 smoke。hash 变化后的自动“重建新 doc”仍保守为 stale + 需要重新 ingest,避免在任意文件保存事件上自动把大文件重新灌入 LightRAG。 + +### 4.12 Source 管理 UI / Query Scope 验证结果 + +2026-06-06 继续补齐 source 管理和查询范围: + +- 资料库问答设置面板的 registry 行新增 `重索引` / `删除索引` 操作。 +- `重索引` 对单个 source 调用 `/api/knowledge-rag/ingest`,用于 stale / retry source 的行级重试。 +- `删除索引` 对单个 source 调用 `/api/knowledge-rag/delete-source`,仍固定 `delete_file=false`,只删除 LightRAG 派生索引,不删除用户 source。 +- `mnote.knowledge_rag.query` / `/api/knowledge-rag/query` 新增 `sourcePaths` 参数,接受 workspace 相对文件或目录;MNote 对 mapped references 做 source scope 过滤。 +- LightRAG `/query/data` 在部分情况下只返回 `data.chunks`,不返回可直接消费的 top-level references;MNote connector 已补 chunks fallback,把 chunk 的 `file_path/reference_id` 生成 reference candidate,再走 registry 映射和 source scope。 +- 修复了一个 stale registry 匹配 bug:没有 `doc_id` 的 reference 不得错误命中 `lightRagDocId=None` 的已删除 entry。 + +新增验证: + +- `cargo test -p mnote-web knowledge_rag -- --nocapture` 通过 17 项;新增覆盖 source scope、chunks fallback reference、已删除 entry 不误匹配。 +- `scripts/task534-knowledge-rag-source-management-scope-smoke.js` 通过:创建 alpha/beta 两个 Markdown sources,`sourcePaths=[alpha]` 时 scoped query 只返回 alpha;浏览器打开资料库问答面板,确认 source row 有行级 `重索引` / `删除索引`;点击 `删除索引` 后 UI 显示已删除,原始 alpha 文件仍存在;删除后 scoped query 不再返回 alpha reference。 + +结论:source 管理 UI 已从“只能批量输入路径”推进到 registry 行级 reindex/delete;query scope 已可按 source 文件或目录过滤 MNote references。排序层已有 query lexical rerank 和 `hybrid -> mix` alias;真实 rerank 模型仍保持关闭,后续仅在样本证明需要时评估。 + +## 5. Provider 合同 + +第一版只需要一个内部 provider 合同,不做完整插件系统。 + +```rust +KnowledgeRagProvider { + status(workspace_id, root_uri) -> ProviderStatus + configure_sources(source_refs, options) -> ConfigureResult + ingest_or_reindex(source_refs, options) -> IngestJob + query(question, scope, options) -> RagAnswer + query_data(question, scope, options) -> RagRetrievalData + open_reference(reference_id, chunk_id?) -> OpenReferencePlan + delete_source(source_ref) -> DeleteResult +} +``` + +LightRAGAdapter 实现该合同。 + +MNote 侧保留: + +- `workspaceId` +- `rootUri` +- `allowedRoots` +- resource source path +- owner document / resource tab mapping +- open action / locator fallback +- citationMarkdown / citationUrl 生成 +- LightRAG dashboard embedding + +LightRAG 侧保留: + +- parser routing +- OCR / MinerU / Docling +- sidecar artifacts +- vector / graph storage +- query modes +- reranker +- parse cache +- task state / graph view / dashboard + +## 6. 初始配置 + +第一阶段优先把 LightRAG 以源码 / host 方式独立跑起来,再让 MNote 通过 connector 访问。不要先改 MNote 主链。 + +部署原则: + +- 7-50 基线:源码编译 / host service。 +- Docker:只用于隔离 demo 或确认官方 dashboard,不作为 MNote connector 默认路径。 +- LightRAG 服务绑定 `127.0.0.1`,不暴露公网。 +- LightRAG storage 放到 MNote 控制范围外的派生缓存目录,例如 `/mnt/Data1T/Mnote_data/lightrag/rag_storage`。 +- LightRAG source 读取 MNote allowed roots 中的真实文件路径;如果官方 scan 只能扫描单一 input dir,先用最小 source spike 确认是否支持 host path / symlink / SDK 直入,再决定是否需要 adapter。 +- 任何 staging / upload 降级都必须带 registry 映射和删除同步,不得成为长期主线。 + +LightRAG 初始配置优先使用官方 MinerU API,减少本地服务数量: + +```bash +HOST=127.0.0.1 +PORT=9621 +CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 +WORKING_DIR=/mnt/Data1T/Mnote_data/lightrag/rag_storage +INPUT_DIR=/mnt/Data1T/Mnote_data/lightrag/inputs + +LIGHTRAG_KV_STORAGE=JsonKVStorage +LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage +LIGHTRAG_GRAPH_STORAGE=NetworkXStorage +LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage + +LLM_BINDING=openai +LLM_BINDING_HOST=http://127.0.0.1:20128/v1 +LLM_MODEL=aigc/qwen-3.5 +LLM_TIMEOUT=180 +MAX_ASYNC_LLM=4 +KEYWORD_MAX_ASYNC_LLM=4 +QUERY_MAX_ASYNC_LLM=4 + +VLM_LLM_BINDING=openai +VLM_LLM_BINDING_HOST=http://127.0.0.1:20128/v1 +VLM_LLM_MODEL=aigc/qwen-3.5 +VLM_MAX_ASYNC_LLM=1 + +EMBEDDING_BINDING=openai +EMBEDDING_BINDING_HOST=http://127.0.0.1:20128/v1 +EMBEDDING_MODEL=nvidia/baai/bge-m3 +EMBEDDING_DIM=1024 +EMBEDDING_TOKEN_LIMIT=8192 +EMBEDDING_SEND_DIM=false +EMBEDDING_FUNC_MAX_ASYNC=8 +EMBEDDING_BATCH_NUM=32 +EMBEDDING_TIMEOUT=60 + +ENABLE_LLM_CACHE=false +ENABLE_LLM_CACHE_FOR_EXTRACT=true + +CHUNK_SIZE=1200 +CHUNK_OVERLAP_SIZE=100 +TOP_K=40 +CHUNK_TOP_K=20 +MAX_ENTITY_TOKENS=6000 +MAX_RELATION_TOKENS=8000 +MAX_TOTAL_TOKENS=30000 +KG_CHUNK_PICK_METHOD=VECTOR +RELATED_CHUNK_NUMBER=5 +COSINE_THRESHOLD=0.2 + +MAX_PARALLEL_INSERT=3 +MAX_PARALLEL_PARSE_NATIVE=5 +MAX_PARALLEL_PARSE_MINERU=2 +MAX_PARALLEL_ANALYZE=3 +MAX_UPLOAD_SIZE=0 + +LIGHTRAG_PARSER=*:native-iteP,*:mineru-iteP,*:legacy-R +MINERU_API_MODE=official +MINERU_API_TOKEN= +MINERU_OFFICIAL_ENDPOINT=https://mineru.net +MINERU_MODEL_VERSION=vlm +MINERU_IS_OCR=false +MINERU_LANGUAGE=ch +MINERU_ENABLE_TABLE=true +MINERU_ENABLE_FORMULA=true +MINERU_POLL_INTERVAL_SECONDS=2 +MINERU_MAX_POLLS=300 +VLM_PROCESS_ENABLE=true +SUMMARY_LANGUAGE=Chinese +ENTITY_EXTRACTION_USE_JSON=true +``` + +当前默认文本 LLM、extract/query 与 VLM 已统一为 `aigc/qwen-3.5`,服务入口为 `http://127.0.0.1:20128/v1`。此前 `deepseek-v4-flash` / Nemotron VLM 结论只保留为历史模型筛选记录,不再作为当前默认配置。embedding 继续使用 OmniRoute / NVIDIA 的 `nvidia/baai/bge-m3`,维度固定为 `1024`,必须在首次 ingest 前确定,后续不能随意改。 + +VLM 候选实测结论: + +- `nvidia/nemotron-nano-12b-v2-vl:free`:OpenRouter 直连可用;红色矩形识别正确约 2.7s,真实杯子图片约 6.1s;曾作为免费 VLM canary 对照,当前默认已切到 `aigc/qwen-3.5`。 +- `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free`:OpenRouter 直连返回空内容,不采用。 +- `nvidia/nemotron-3.5-content-safety:free`:主要返回安全分类,不适合作通用 VLM。 +- OmniRoute / NVIDIA 的 Nemotron VLM 路线:当前多返回图片 unavailable / 无法查看图片,暂不采用。 +- `agnes-2.0-flash`:文本问答可用;图像输入可识别简单图片,但 VLM 延迟不稳定,真实图片可到 80s+,降为备选。 +- `geminipay/gemini-3.1-flash-lite-preview`:通过简单图像输入测试,响应约 1s,但属于收费账号,不作为默认。 +- `cx/gpt-5.4-mini` / `codex/gpt-5.4-mini` / `xx/gpt-5.4-mini`:可调用,但对图像回答“无法判断”,不适合作为 VLM 默认。 +- `agnes-image-2.0-flash` / `agnes-image-2.1-flash` 属于图像生成 / 编辑方向,不是 LightRAG 所需的 image-to-text VLM;按 chat completions 测试返回 404,暂不纳入 LightRAG VLM。 + +复杂截图补测: + +- 测试图:MNote Page AI 复杂网页截图,压缩到 `888x650`、约 `41KB`。 +- `nvidia/nemotron-nano-12b-v2-vl:free`:约 `5.9s`,能提取中心标题和关键 token,但把右侧标题误读为“页面截面视图”。 +- `agnes-2.0-flash`:约 `9.6s`,中心标题、右侧“页面 AI”和 token 提取更准确。 +- 历史结论:Nemotron 免费 VLM 速度和可用性较好,曾适合作 canary;当前默认已切到 `aigc/qwen-3.5`,高精度 UI/OCR 场景仍需继续用样本评估。 + +本地 embedding 平台补测: + +- 本机 Ollama 已有 `bge-m3:latest`,embedding 维度 `1024`,模型 F16,约 `1.1GB`,当前通过 Ollama 100% CPU 执行。 +- 64 段中文文本 embedding:Ollama 本地 `bge-m3` 约 `33.7s`;OmniRoute / NVIDIA `nvidia/baai/bge-m3` 约 `1.6s`。 +- Ollama 运行 `bge-m3` 后新增 runner RSS 约 `1.3GB`,GPU 显存基本不变。 +- 当前不建议切本地 embedding 默认;若要本地化,应优先评估 llama.cpp / CUDA 或 quantized GGUF server,而不是直接用 Ollama F16 CPU 路线。 + +Embedding / rerank 当前建议: + +- 默认 embedding 继续使用 `nvidia/baai/bge-m3`,不要在正式样本 ingestion 前频繁切换;embedding 模型或维度变化会要求重建索引。 +- 本地 embedding 只保留为后续优化方向,优先路线是 `llama.cpp + bge-m3 GGUF + CUDA`;Ollama F16 CPU 路线不适合作默认批量建库。 +- 备选 embedding 中,`nvidia/nv-embedqa-e5-v5` 实测可用且维度 `1024`,但需要 `input_type=query/passage` 这类 asymmetric 参数;LightRAG 接入复杂度高于当前 `bge-m3`,暂不切换。 +- `openrouter/openai/text-embedding-3-small` 可用但维度 `1536`,切换会改变索引维度;当前不采用。 +- rerank 先保持 `RERANK_BINDING=null`。OmniRoute 中可见 `nvidia/nv-rerankqa-mistral-4b-v3`,但当前 `/v1/rerank`、`/rerank`、`/v2/rerank`、`/v1/ranking` 等常见端点实测 404,暂不能直接按 LightRAG rerank binding 配。 +- 若真实样本出现“召回足够但排序差”,优先本地评估 `bge-reranker-v2-m3` 或 `Qwen3-Reranker-0.6B`,通过 Cohere-compatible / vLLM-compatible 服务接入 LightRAG。 + +不要直接套用泛化配置里的旧变量名: + +- `LLM_API_KEY` / `LLM_API_BASE` 应改为 LightRAG v1.5 的 `LLM_BINDING_API_KEY` / `LLM_BINDING_HOST`。 +- `EMBEDDING_API_KEY` / `EMBEDDING_API_BASE` 应改为 `EMBEDDING_BINDING_API_KEY` / `EMBEDDING_BINDING_HOST`。 +- `SERVER_HOST` / `SERVER_PORT` 应改为 `HOST` / `PORT`。 +- `CHUNK_OVERLAP` 应改为 `CHUNK_OVERLAP_SIZE`。 +- `KV_STORAGE` / `VECTOR_STORAGE` / `GRAPH_STORAGE` / `DOC_STATUS_STORAGE` 应改为 `LIGHTRAG_*_STORAGE`。 +- `TEXTRACT_USE_OCR` / `OCR_ENGINE` / `OCR_WORKERS` 不是当前 LightRAG v1.5 API server 的 MinerU 路线变量;当前应使用 `LIGHTRAG_PARSER` + `MINERU_API_MODE` + `MINERU_*`。 + +`ENABLE_LLM_CACHE=false` 是有意设置:source delete / stale 语义验证前,不缓存最终问答,避免删除源后自然语言回答仍从 cache 命中。`ENABLE_LLM_CACHE_FOR_EXTRACT=true` 保留,只缓存抽取过程,降低重复建库成本。 + +后续大量书籍批处理时再评估本地 MinerU: + +```bash +MINERU_API_MODE=local +MINERU_LOCAL_ENDPOINT=http://localhost:8000 +``` + +默认 query 策略: + +- 宽泛跨书总结:`mode=mix` 或 `mode=global`,开启 rerank。 +- 指定文档细节:先在 MNote scope 限制到 source,再用 LightRAG query。 +- 只要需要可点击来源,必须走 `query_data` 或补充读取 chunk/sidecar,不能只消费自然语言 answer。 + +第一阶段可接受 MNote 只打开 LightRAG 自身 UI / dashboard,先验证 ingestion、OCR、query、graph view 和引用质量;MNote 内嵌 UI 延后。 + +## 7. LiteParse 退役结果 + +LiteParse 已从默认路径退役: + +- [x] LightRAG 能处理当前样本 PDF / DOCX / 图片 PDF。(PDF / 图片 PDF 已跑通;DOCX 已通过 `task532` 验证 ingestion / query / resource tab fallback citation) +- [x] MinerU official token 路线在 MNote secret 注入后可跑通。 +- [x] LightRAG sidecar `blockid -> positions -> page/bbox` 能映射到 MNote EvidenceLocator。(已通过扫描 PDF smoke;当前为 quote/block 文本匹配 fallback,不是 refs 无损映射) +- [x] 查询结果能返回可点击 citation,打开到 resource tab / page / bbox。(`task529-knowledge-rag-citation-resource-tab-smoke` 已通过) +- [x] 旧 evidence search 已完成退役前回归,随后 `/api/evidence/search` / agent evidence tools 从默认路径移除。 +- [x] 删除或重建知识库不会删除用户原始文件。 +- [x] 旧 LiteParseProvider 的测试覆盖有替代路径或明确归档说明。(见 7.1;LightRAG PDF/DOCX/扫描 PDF 路线作为新资料默认) + +### 7.1 LiteParse -> LightRAG Cutover + +当前切换策略是“active 退役 + recycle 软归档 + 新资料默认 LightRAG”: + +| 代码 / 能力 | 当前状态 | Cutover 决策 | +| --- | --- | --- | +| `evidence_parse.rs` / `LiteParseProvider` | active provider 返回 `liteparse_retired`;历史实现软归档到 `recycle/20260606-liteparse-retirement/` | 不再调用 `lit` / `liteparse` CLI | +| `local_search_index.rs` 的资源 parse sidecar 入库 | 不再为 PDF / Office 调用 LiteParse parse sidecar | PDF / Office / 图片 OCR 走 LightRAG source registry + query | +| `/api/evidence/search`、`mnote.evidence.search/read/open` | `/api/evidence/search` 返回 retired;agent manifest 不再暴露 evidence tools | 使用 `mnote.knowledge_rag.query/open_reference` | +| `/api/local-folder/ocr/*` 与 `.mnote/ocr-index.json` 检索 | HTTP API 返回 retired;active UI 不再调用旧 OCR sidecar;`includeOcr` 不再追加 OCR sidecar 搜索结果 | 图片 / PDF / Office 统一通过 `mnote.knowledge_rag.status/ingest/query/delete-source` | +| `skills/mnote-local-index/SKILL.md` | 软归档到 recycle;active capability pack 删除 | 兼容 alias 映射到 `mnote-knowledge-rag` | +| FileTree `data-index-status` | 从 LightRAG registry 读取 indexed / indexing / failed | 旧 evidence.sqlite / OCR index 不再决定文件树勾选状态 | +| `skills/mnote-knowledge-rag/SKILL.md` | 新资料库问答优先走 LightRAG | 作为多书 / 多论文 / PDF / DOCX 的默认入口 | +| 7-46 / 7-48 evidence 设计稿 | 仍解释旧 sidecar / evidence index 生命周期 | 保留为历史解释 / migration note | + +执行顺序: + +1. 退役 LiteParse active provider 和 agent evidence / local-index capability。 +2. 顶栏 OCR / 本地索引入口并入 LightRAG 资料库问答入口。 +3. FileTree 勾选 / 失败状态从 LightRAG registry 读取。 +4. FileTree 右键“生成 OCR”替换成“加入资料库索引”。 +5. 资源页 / Page AI target / Reasonix ACP 不再调用旧 OCR/evidence/index 工具。 +6. 历史 LiteParse / local OCR 代码、skill、smoke 软归档到 recycle。 + +退役顺序: + +1. 新导入资料默认走 LightRAG。 +2. Agent 工具默认只暴露 LightRAG knowledge RAG。 +3. FileTree 状态绑定 LightRAG source registry。 +4. 历史 LiteParse sidecar 只保留 migration note,不再作为 UI / agent fallback。 + +### 7.2 MNote / LightRAG Coupling Review Fix + +2026-06-06 人工复核后继续收口 MNote 与 LightRAG 的耦合状态: + +- 顶栏只保留 1 个 `资料库问答` 入口;历史 OCR task toggle 不再由 resource tab runtime 动态创建。 +- 删除 source 后,`/api/knowledge-rag/delete-source` 会把 registry 标为 `delete_submitted` 或 `delete_completed`;后续 `/api/knowledge-rag/status` 会对照 LightRAG `/documents` 自动把已消失的 doc 标为 `delete_completed`。 +- FileTree 状态语义调整为:`indexed` 绿灯、`indexing` 黄灯、`failed` 红灯、未索引或 `delete_completed` 无灯;`delete_submitted` 作为黄灯,不再被误显示成失败。 +- 新增 `/api/knowledge-rag/prune-registry` 与 UI `清除失效记录`,用于一键清理已删除 / 过期 / 失败的 registry 记录;仍在 `delete_submitted` 的记录不会被清掉,避免隐藏 LightRAG 后台删除尚未确认的状态。 +- 新增索引输入去重,同一批重复 source 不会重复提交;相同 `sourcePath + sourceHash` 已登记且未过期时,非 `force` ingest 会返回 `already_registered`。 +- 新增索引 / 删除 / prune 成功后会触发 `mnote:knowledge-rag-source-updated`,设置面板和 FileTree 会立即刷新;LightRAG busy 时也会刷新 registry 并把 source 保持为等待 / 处理中状态。 +- 设置面板不再默认显示空输入行的 `待索引` 标签,避免与上方 source registry 状态重复。 +- Dashboard URL 在 LAN 访问 MNote 时会把 `127.0.0.1` / `localhost` 改写为当前 MNote host;如果 LightRAG 服务自身仍只绑定 loopback,下一步应改 LightRAG bind host 或做 MNote 反向代理。 +- 2026-06-06 追加复核:LightRAG 已切到 `aigc/qwen-3.5`,MNote status 面板会显示当前 LLM model;`/api/knowledge-rag/status` 额外返回 LightRAG `/documents` 摘要和状态分组,UI 同时显示 `MNote 登记来源数` 与 `LightRAG 文档数`,并把未映射到 MNote registry 的 LightRAG 文档列为 `LightRAG 未映射` 分类。 +- 资料库设置 UI 调整为“操作在上、列表在下”,来源列表新增分类过滤:`需处理`、`索引中`、`失败/过期`、`已索引`、`已移除`、`LightRAG 未映射`、`全部`;默认优先显示需处理项,减少成功项噪音。 +- 补齐 `travel_explore` 本地 SVG mask,避免资料库入口在 Material Symbols 字体不可用时显示成黑色方块。 + +本轮验证: + +- `node --check` 覆盖 `sidebar-page-settings-runtime.js`、`sidebar-tree-runtime.js`、`document-resource-tab-runtime.js`、`sidebar-tree-live-apply-runtime.js`。 +- `cargo check -p mnote-web` 通过。 +- `cargo test -p mnote-web knowledge_rag -- --nocapture` 通过 21 项。 +- `cargo test -p mnote-web local_file_tree_marks_lightrag_indexed_indexing_and_failed_source_files -- --nocapture` 通过,覆盖 indexed / indexing / failed / delete_submitted / delete_completed。 +- `cargo test -p mnote-web sidebar_settings_runtime_routes_index_and_ocr_to_lightrag_settings -- --nocapture` 与 `page_layout_exposes_lightrag_knowledge_rag_settings_only` 通过。 +- 浏览器复核 `http://127.0.0.1:3000/`:topbar `资料库问答` 入口数量为 1,旧 OCR / 本地索引入口数量为 0;面板显示 `LightRAG healthy`、有 `清除失效记录`,空输入行不显示 `待索引`;旧 deleted registry 已同步为 `LightRAG 已移除` 且 FileTree 无灯。 + +### 7.3 Status Bridge / Image Wrapper Follow-up + +2026-06-07 复核确认:LightRAG 切到 `aigc/qwen-3.5` 后索引速度恢复,但 MNote 仍必须主动同步后台完成状态,不能假设 LightRAG 会向 MNote 推送事件。 + +当前补充口径: + +- LightRAG 当前文本 LLM、extract/query 与 VLM 统一使用 `aigc/qwen-3.5`;VLM host 为 `http://127.0.0.1:20128/v1`,status 为 healthy。此前 `deepseek-v4-flash` 只作为历史调试背景,不再作为当前默认模型口径。 +- MNote ingest / reindex 后启动有界 status bridge/backoff,对照 LightRAG `/documents` 同步 `processing/submitted -> processed/failed`,再刷新 source registry、资料库设置面板和 FileTree 灯号;这不是长期无界轮询。 +- LightRAG Documents 页面看不到原始图片文件是预期边界:当前 `/documents/scan` 不直接索引 `png/jpg`,MNote 为图片 source 生成 Markdown wrapper,再由 LightRAG 索引 wrapper 文档,同时 registry 仍映射回原始图片路径。 +- `MNote 登记来源数` 与 `LightRAG 文档数` 应优先看是否一一映射;未映射文档只作为 LightRAG 残留 / 外部手动导入提示,不作为 MNote source truth。 +- 清空历史时只清 LightRAG 派生 documents / input wrapper / parsed cache 与 MNote registry,不删除 workspace 原始资料。 + +## 8. 不做事项 + +- 不继续开发 7-49 的 `mnote.understanding.*` 工具。 +- 不继续开发自研 understanding.sqlite。 +- 不做 mindmap projection 作为知识库默认视图。 +- 不默认接入 PageIndex。 +- 不把 Understand Anything dashboard 作为 MNote 主 dashboard。 +- 不同时默认维护 LiteParse + LightRAG + PageIndex 多套 provider。 +- 不让 LightRAG 写用户 Markdown 正文。 +- 不把 LightRAG sidecar 当用户可编辑真相。 +- 不让 MNote 为 LightRAG 结果再建一套 RAG 索引。 +- 不为了 LightRAG 改动 MNote 文件树、编辑器、普通搜索的主路径。 + +## 9. 插件系统判断 + +当前不开发完整插件系统,但按插件边界实施。 + +原因: + +- 近期真实需要的知识库 provider 只有 LightRAG 一个。 +- 过早插件化会引入 manifest、生命周期、权限、版本、升级、隔离和 UI 管理成本。 +- MNote 现在更需要稳定 provider boundary,而不是多 provider marketplace。 +- LightRAG 已经有自己的任务、图谱、解析和 dashboard 能力,MNote 不应复制一套控制面。 + +第一版只做“无害集成”: + +- 配置项:LightRAG endpoint / working dir / MinerU token secret key / parser mode。 +- 状态页:provider status、job status、storage path、最近错误、dashboard URL。 +- 工具层:query / ingest / reindex / delete source / open reference。 +- UI 层:先外链或 iframe 嵌入 LightRAG dashboard,用于任务控制、graph 查看和 ingestion 状态。 + +当出现第二个稳定 provider,且必须和 LightRAG 并存时,再抽正式插件系统。 + +## 10. 实施 Checklist + +### A. 退役 7-49 + +- [x] 7-49 从 active `design/07-ai/process` 移出。 +- [x] 7-49 标记为 `[recycle]`。 +- [x] 撤回 `mnote.understanding.*` route / tool / skill 接线。 +- [x] 删除未落地主线的 `understanding.rs` untracked 代码。 +- [x] 7-50 明确不再沿 Docker staging 作为 MNote connector 基线。 + +### B. LightRAG Spike + +- [x] 固定 LightRAG 参考版本和源码 / host 运行方式。 +- [x] 建立最小 LightRAG server / SDK 本地运行脚本。 +- [x] 验证 LightRAG 源码运行依赖版本;如 Python 3.14 不兼容,改用专用 Python 3.12/3.13 venv。 +- [x] 验证 OmniRoute OpenAI-compatible 模型在 LightRAG client 下可用;当前默认已从历史 `deepseek-v4-flash` 切到 `aigc/qwen-3.5`。 +- [x] 挑选快速 / 便宜的 OmniRoute 视觉模型,`geminipay/gemini-3.1-flash-lite-preview` 可用但因收费不作为默认。 +- [x] 筛选 OpenRouter 免费 VLM 与 Agnes 免费模型;当前默认已统一为 `aigc/qwen-3.5`,历史 VLM canary 仅保留为对照记录。 +- [x] 验证 `nvidia/baai/bge-m3` embedding 维度和 LightRAG 配置一致。 +- [x] 测试本地 Ollama `bge-m3` embedding 代价,并与 OmniRoute / NVIDIA embedding 对照。 +- [x] 筛选 embedding / rerank 候选并冻结当前默认:embedding 继续 `nvidia/baai/bge-m3`,rerank 暂不开。 +- [x] 使用复杂 MNote UI 截图补测 VLM OCR / UI 理解能力。 +- [x] 接入 MinerU official token,不把 token 暴露给前端。 +- [x] 对照外部 env 建议,补齐 LightRAG v1.5 实际识别的 storage / chunk / query / concurrency / MinerU 参数。 +- [x] 用 symlink scan 跑通受控 source 文件 ingestion,避免默认复制 source 内容。 +- [x] 明确 source registry 字段和 stale / deleted 过滤规则。 +- [x] 验证 `query_data` 返回 chunk/reference。 +- [x] 验证 `DELETE /documents/delete_document` 删除后旧 source 不再可检索。 +- [x] 验证 SDK / upload / symlink / scan 四种路径中哪一种最适合 MNote connector 长期使用。 +- [x] 验证 MNote source 删除后能触发 LightRAG delete,不删除用户原始文件。(显式 `delete-source` route 与 watcher stale sync 均已覆盖;watcher smoke 见 `task533`) +- [x] 用 1 本书籍 PDF、1 个扫描 PDF、2 篇论文跑 ingestion。 +- [x] 保存 sidecar 样本到 `tmp/` 或测试 fixture,不提交敏感原文。(样本保留在 LightRAG `inputs/__parsed__` 派生缓存,不提交) +- [x] 验证 LightRAG dashboard 能查看任务、graph、文档状态。(`task531-lightrag-dashboard-ui-smoke.js` 已通过:Documents 显示 completed/fail/doc summary,Knowledge Graph / Retrieval 均显示 connected) +- [x] 验证 chunk 能反查 sidecar 并生成 locator。(当前不是 refs 无损映射,而是 `file_path + quote/chunk content` 匹配 sidecar block) +- [x] 验证 sidecar position 能生成 MNote EvidenceLocator。(`task529` 已验证 PDF resource tab / page / bbox / 高亮) + +### C. MNote Adapter + +- [x] 新增内部 `KnowledgeRagProvider` 合同。 +- [x] 新增 `LightRAGAdapter`,只封装 HTTP / SDK 调用,不复制 LightRAG 逻辑。(当前落为 `knowledge_rag` route adapter,未额外抽 trait) +- [x] 新增 provider status route。 +- [x] 新增 dashboard URL / open route。 +- [x] 新增 ingestion / reindex job 控制 route。 +- [x] 新增 query route。 +- [x] 新增 open reference resolver。 +- [x] Hermes / Page AI 只看到 MNote tool,不直接看到 LightRAG 内部路径。 +- [x] MNote 普通页面搜索、文件树、编辑器不依赖 LightRAG 可用性。 +- [x] Connector 避免被历史 `LIGHTRAG_API_KEY` 污染,优先使用 MNote 专用 key 或 LightRAG source `.env`。 + +### D. UI / Agent 闭环 + +- [x] Page AI 能选择“资料库问答”模式。(通过 `mnote-knowledge-rag` capability pack 暴露) +- [x] Agent skill 只暴露少量知识库工具:status、query、open_reference。(reindex/ingest 暂只保留 HTTP route,不暴露给只读 agent) +- [x] 回答必须带 citations。(skill 要求引用 `references` / `citationMarkdown`) +- [x] citation 能打开到原文 resource tab。(`task529` 已通过) +- [x] citation 能展示 page / bbox / quote 降级状态。(已返回 `chunkId` / `quote`,并能在扫描 PDF 样本生成 page/bbox;未命中时继续降级,不伪造) +- [x] 没有 locator 时明确显示“来源定位降级”,不伪造页码。 +- [x] Page AI 真实回答能使用 RAG citation,而不是只验证 tool 层。(`task530-knowledge-rag-page-ai-final-answer-smoke.js` 已通过:Reasonix 最终可见回答引用 `scan-image-start.pdf · p.1` 可点击 resourceTab 链接) +- [x] MNote 中可以进入 LightRAG UI 进行任务控制和 graph 查看。(通过 MNote `/api/knowledge-rag/status` 的 `dashboardUrl` 打开 WebUI,`task531` 已浏览器验证 Documents / Knowledge Graph / Retrieval) + +### E. LiteParse 退役 + +- [x] 对照 LiteParse 当前能力和 LightRAG sidecar 能力。(见 7.1) +- [x] 列出仍依赖 LiteParse 的代码路径。(见 7.1) +- [x] 把 LiteParse 默认 provider 切到 LightRAG,不再 fallback 到 evidence。 +- [x] 删除 active UI 中的 LiteParse / 本地索引 / OCR 专属入口。 +- [x] FileTree 勾选 / 失败状态改为读取 LightRAG source registry。 +- [x] 软归档 LiteParse / local OCR 历史代码与 smoke 到 `recycle/20260606-liteparse-retirement/`。 + +### F. 当前剩余闭环 + +当前 7-50 v1 已可归档;下面是本轮收口项与非阻塞后续: + +- [x] LightRAG 资料库设置 UI:不能混入现有 MNote 本地索引设置;需要独立 surface,显示 provider status、dashboard URL、working dir、input dir、source registry、最近错误。(已落独立 topbar 入口与状态面板) +- [x] LightRAG source 管理 UI:支持从当前 local-folder 中选择要索引的文件 / 目录,调用 `/api/knowledge-rag/ingest`,展示 indexing / processed / stale / deleted / retryRequired 状态。(已支持手填、从 filetree 选中项填入路径、输入行状态、registry 行级 reindex/delete;`task534` 已浏览器验证) +- [x] LightRAG dashboard 接入:第一步可以外链打开 `dashboardUrl`,第二步再评估 iframe;必须浏览器验证 documents / graph / task 状态可见。(`task531` 已完成外链 dashboard 的 documents / graph / retrieval 验证;iframe 仍作为后续体验评估,不阻塞 v1) +- [x] source watcher 自动同步:source 删除 / 移动 / 重命名 / hash 变化后,自动标记 registry stale,并触发 delete / reindex;不能只依赖显式 `delete-source` route。(`task533` 验证删除事件;hash 变化有单测;重命名 / 移动复用旧 path missing 逻辑;自动重建新 doc 当前保守为 stale + 重新 ingest) +- [x] 查询 scope 与排序:支持按选中 source / 目录限制 RAG 查询;`hybrid -> mix` alias 和 MNote lexical rerank 已先落地,仍需评估 `mix/global/local`、`topK/chunkTopK` 与 rerank。(`sourcePaths` 已落 route / Hermes / Reasonix schema,`task534` 验证按 source 文件过滤;rerank 仍按 7-50 结论默认关闭) +- [x] Page AI 真实回答 smoke:不仅验证 Hermes tool 能返回 references,还要验证 Page AI 最终回答使用 `citationMarkdown`,不会把 retrieval raw 当最终答案,也不会伪造页码。(`task530` 已通过) +- [x] DOCX / Office 样本 ingestion:补测 LightRAG 对 DOCX / Office 附件的解析与 citation 回跳能力。(`task532` 已验证 DOCX 解析、marker 召回、registry 映射和 DOCX resource tab fallback citation;PPTX/XLSX 未纳入本轮) +- [x] 旧 evidence search 退役:保留历史回归证据,但 active route / agent tool 不再作为 fallback;新检索与 OCR / 索引统一走 LightRAG。 +- [x] LiteParse 退役设计:见 7.1;结论已从冻结 fallback 更新为 active 退役 + recycle 软归档。 + +## 11. 验收标准 + +- 用户能把多本书 / 多篇论文纳入同一个资料库。 +- LightRAG 可以在不改动 MNote 普通功能的情况下完成 OCR、parse、index、query。 +- LightRAG 读取的是 MNote 允许的真实 source,或有明确 registry 映射的临时 staging;不能存在无人管理的第二份资料真相。 +- agent 能回答跨资料问题。 +- 回答不能只给“看起来合理”的总结,必须有来源。 +- 至少 PDF 页码和 chunk quote 可追溯。 +- 对支持 bbox 的 sidecar,能打开到页面区域或保留 bbox metadata。 +- 删除知识库索引不会删除用户原始文件。 +- 删除 / 移动 / 重命名 source 后,旧索引不会继续作为有效来源参与回答;删除同步失败时必须显示 stale 状态。 +- MNote active 代码中不再出现未完成的 `mnote.understanding.*` 主线接线。 +- LightRAG 不可用时,MNote 文件树、编辑器、普通页面搜索仍可正常使用。 diff --git a/design/old/07-ai/process/7-49-local-understanding-graphrag-kernel-v1.md b/design/old/07-ai/process/7-49-local-understanding-graphrag-kernel-v1.md new file mode 100644 index 00000000..251e842a --- /dev/null +++ b/design/old/07-ai/process/7-49-local-understanding-graphrag-kernel-v1.md @@ -0,0 +1,643 @@ +# 7-49 [recycle] Local Understanding GraphRAG Kernel v1 + +> 创建时间:2026-06-06 +> +> 当前状态:`RECYCLED` +> +> 退役时间:2026-06-06 +> +> 退役原因:该稿以自研 Understanding GraphRAG Kernel / mindmap projection 为主线,已被 LightRAG 单外挂知识库问答路线取代。相关代码接线已撤回;后续不再按本文推进。 +> +> Owner:07-ai / 03-rust-web / control-plane / 01-tree-first-graph-kernel +> +> 上位依据: +> - `/mnt/Data1T/mnote/ARCHITECTURE.md` +> - `/mnt/Data1T/mnote/CURRENT_ARCHITECTURE.md` +> - `/mnt/Data1T/mnote/design/07-ai/process/7-46-document-evidence-retrieval-kernel-v1.md` +> - `/mnt/Data1T/mnote/design/07-ai/process/7-48-paperless-ngx-reference-resource-ingestion-job-index-v1.md` +> - `/mnt/Data1T/mnote/design/03-rust-web/done/3-25-local-folder-mineru-ocr-sidecar-v1.md` +> - `/mnt/Data1T/mnote/design/07-ai/process/7-18-local-first-agent-file-editing-control-plane-v1.md` +> +> 参考来源: +> - `reference-code/Understand-Anything` +> - `reference-code/PageIndex` +> - `reference-code/BookRAG` +> - `reference-code/Kwipu` +> - `sqlite-graphrag` / `sqlite-knowledge-graph` 类本地 GraphRAG 方案 + +## 1. 第一结论 + +MNote 需要新增一层 `Local Understanding GraphRAG Kernel`。它不是替换当前 evidence search,而是在 `EvidenceLocator` 之上生成可查询的本地理解图,让 agent 能回答跨章节、跨资料的系统性问题。 + +目标链路: + +```text +原始文件 / Markdown / PDF / Word / OCR provider result + -> Evidence Kernel: quote + locator + source-map + -> Structure Tree: document / section / page / table / list + -> Understanding Graph Builder: concept / entity / claim / relation + -> SQLite GraphRAG Store: graph + FTS + embedding + locator + -> Query Planner: simple / complex / global + -> Evidence-backed Answer: answer item -> graph edge -> EvidenceLocator +``` + +主适配策略: + +- `Understand Anything`:作为“通用理解图生成器”的工程骨架参考,采用扫描、确定性抽取、LLM 语义抽取、graph reviewer、增量更新和 JSON graph artifact 思路。 +- `sqlite-graphrag / sqlite-knowledge-graph`:作为本地持久化和查询引擎参考,默认落 SQLite,不新增常驻 graph 服务。 +- `PageIndex / BookRAG`:只作为算法参考,借鉴 structure tree、tree search、query planning、hierarchy + graph + evidence mapping,不直接集成其运行时。 +- 默认 LLM runtime:Understanding Graph 的语义抽取和 reviewer 默认走 OmniRoute combo `DeepSeek-v4-flash`,用于充分利用当前 NVIDIA 免费资源;但 provider / combo / model 必须可配置,不允许写死到 schema 或 graph 数据中。 + +关键修正:**OCR 结果、parse 结果、source-map、搜索索引和理解图都是派生数据,默认不应写入用户正文目录。** 它们应写入 MNote 管理的 SQLite / artifact store / graph store,并通过 locator 回到用户原始文件。 + +## 2. 数据真相与派生数据边界 + +### 2.1 真相分层 + +| 数据 | 默认真相 | 默认存放 | 说明 | +| --- | --- | --- | --- | +| 用户 Markdown 正文 | 用户 `.md` 文件 | 用户 workspace | 可编辑、可同步、可备份 | +| 用户附件 / PDF / Word / 图片 | 用户原始文件 | 用户 workspace | MNote 不替代原始资源 | +| OCR 文本 | 派生 artifact | MNote SQLite / artifact store | 可删除重建,不污染用户目录 | +| parse 文本 | 派生 artifact | MNote SQLite / artifact store | 可删除重建 | +| source-map / bbox / page map | 派生定位 artifact | MNote SQLite / artifact store | 可由 parser/OCR 重建 | +| search index | 派生索引 | SQLite FTS / graph store | 不作为正文真相 | +| understanding graph | 派生理解图 | SQLite graph tables | node/edge 必须能追溯 locator | + +### 2.2 不再默认写用户目录 + +旧方案曾把 OCR 结果写入 owner Markdown 同目录的 `{pageStem}.ocr/`,并把本地索引写入 `.mnote/index/`。这解决了可见性和可迁移性,但会把大量机器派生数据混入用户目录: + +- 用户文件树被 OCR / parse / source-map / index 污染。 +- 同步、备份、搜索、分享时很难区分正文和派生物。 +- 多用户 / share / team workspace 中,派生数据权限和正文权限容易混淆。 +- graph / index rebuild 后会产生大量无业务意义的文件变更。 + +新口径: + +- 用户目录只保存用户原始内容和用户显式插入的引用。 +- OCR / parse / index / graph 默认落到 MNote 管理存储。 +- 如果用户需要导出 OCR 文本,可提供显式 `export OCR` 或 `insert OCR link/content`,但这不是默认路径。 +- 当前阶段旧 OCR sidecar 和旧 `.mnote/index` 都视为测试派生数据,不做兼容读取,不做迁移导入;切到新存储后可直接清理。 + +## 3. 推荐存储形态 + +### 3.1 Control-plane artifact store + +派生文本和 source-map 不建议全部塞进普通 control-plane 表,也不建议回到用户目录。第一版可以用 SQLite + content-addressed blob 表: + +```sql +derived_artifacts( + artifact_id TEXT PRIMARY KEY, + workspace_id TEXT NOT NULL, + root_uri TEXT NOT NULL, + owner_document_id TEXT, + owner_document_path TEXT, + source_root_relative_path TEXT NOT NULL, + artifact_kind TEXT NOT NULL, -- ocr_text | parse_text | source_map | page_image | thumbnail + provider TEXT NOT NULL, + source_hash TEXT NOT NULL, + content_hash TEXT NOT NULL, + content_text TEXT, + content_blob_path TEXT, + mime_type TEXT, + size_bytes INTEGER, + created_at_ms INTEGER NOT NULL, + updated_at_ms INTEGER NOT NULL, + stale INTEGER NOT NULL DEFAULT 0 +); +``` + +原则: + +- 小文本可以直接 `content_text`。 +- 大 source-map / page image / provider 原始结果可进入 MNote 管理的 artifact blob 目录,表内只存 hash 和路径。 +- blob 目录必须在 MNote data root 下,而不是用户 workspace 下。 +- artifact 必须用 `workspace_id + root_uri + source_root_relative_path + source_hash` 绑定原始文件版本。 + +### 3.2 Evidence / GraphRAG SQLite + +旧 `.mnote/index/evidence.sqlite` 不迁移为新真相。新 evidence / understanding store 应直接创建在 MNote 管理存储,例如: + +```text +/mnt/Data1T/Mnote_data/control-plane/indexes/{workspace_hash}/evidence.sqlite +/mnt/Data1T/Mnote_data/control-plane/indexes/{workspace_hash}/understanding.sqlite +``` + +或统一放到 control-plane DB 的附属数据库中: + +```text +ATTACH '.../indexes/{workspace_hash}/evidence.sqlite' AS evidence; +ATTACH '.../indexes/{workspace_hash}/understanding.sqlite' AS understanding; +``` + +第一版建议仍使用独立 SQLite 文件,避免 control-plane 主库被大 FTS / embedding / graph 写放大拖慢。 + +## 4. Understanding Graph Schema + +### 4.1 Node + +```sql +understanding_node( + node_id TEXT PRIMARY KEY, + workspace_id TEXT NOT NULL, + root_uri TEXT NOT NULL, + node_kind TEXT NOT NULL, -- document | section | page | table | list | concept | entity | claim | method | condition + canonical_name TEXT NOT NULL, + display_name TEXT NOT NULL, + summary TEXT, + language TEXT, + metadata_json TEXT NOT NULL DEFAULT '{}', + evidence_locator_json TEXT, + source_artifact_id TEXT, + confidence REAL NOT NULL DEFAULT 1.0, + created_by TEXT NOT NULL, -- deterministic | llm | reviewer | user + source_hash TEXT NOT NULL, + updated_at_ms INTEGER NOT NULL +); +``` + +### 4.2 Edge + +```sql +understanding_edge( + edge_id TEXT PRIMARY KEY, + workspace_id TEXT NOT NULL, + root_uri TEXT NOT NULL, + from_node_id TEXT NOT NULL, + to_node_id TEXT NOT NULL, + edge_kind TEXT NOT NULL, + relation_label TEXT NOT NULL, + evidence_locator_json TEXT, + source_artifact_id TEXT, + confidence REAL NOT NULL DEFAULT 1.0, + created_by TEXT NOT NULL, + source_hash TEXT NOT NULL, + updated_at_ms INTEGER NOT NULL +); +``` + +通用 `edge_kind`: + +- `document_contains_section` +- `section_contains_page` +- `section_contains_table` +- `section_contains_claim` +- `concept_has_subtype` +- `concept_has_example` +- `concept_defined_by` +- `entity_has_property` +- `method_has_condition` +- `claim_supported_by` +- `table_row_supports_claim` +- `term_alias_of` +- `mentions` +- `related_to` + +注意:不为“保护基”这种单一领域写专用 schema。化学、生物、财务、法律文档都应落到通用 concept / entity / claim / property / condition / evidence 关系上。 + +### 4.3 Search Surfaces + +SQLite store 同时提供: + +- FTS:`node.display_name / summary / claim text / artifact text`。 +- embedding:可选,先作为 table/provider 抽象,不强制第一版。 +- graph query:固定 relation traversal。 +- tree query:按 document / section / page / table 聚合。 + +## 5. Builder Pipeline + +### 5.1 确定性抽取 + +默认不先调用 LLM,先从 artifacts 做可重复抽取: + +- Markdown headings / wikilinks / frontmatter / resource links。 +- PDF / Word source-map 的 page、section、table、list。 +- OCR / parse artifact 的段落、页、表格行。 +- 文件系统资源归属、owner document、source hash。 + +输出: + +```text +document -> section -> page -> block/table/list +``` + +### 5.2 LLM 语义抽取 + +LLM 只做确定性结构无法得到的语义层: + +- concept / entity。 +- claim / definition。 +- subtype / example / alias。 +- method / condition / property。 +- table row 到 claim 的解释性关系。 + +LLM 输出必须是结构化 JSON,且每条 node/edge 必须带输入 evidence id 或 locator。 + +默认 LLM runtime: + +```json +{ + "understandingGraph": { + "semanticExtractor": { + "provider": "omniroute", + "routeKind": "combo", + "comboName": "DeepSeek-v4-flash", + "modelHint": "deepseek-v4-flash", + "purpose": "semantic_extraction", + "fallback": "deterministic_only" + }, + "graphReviewer": { + "provider": "omniroute", + "routeKind": "combo", + "comboName": "DeepSeek-v4-flash", + "modelHint": "deepseek-v4-flash", + "purpose": "graph_review", + "enabledByDefault": false + } + } +} +``` + +配置原则: + +- `DeepSeek-v4-flash` 只作为默认推荐 combo,理由是适合后台异步批处理,并能优先利用当前 NVIDIA 免费资源。 +- 实际 runtime 通过 OmniRoute combo / model registry 解析当前可用 ID,例如 `deepseek-v4-flash` 或 provider-scoped `deepseek/deepseek-v4-flash`,MNote 侧不把具体上游 ID 写死进 graph schema。 +- MNote 不直接持有外部 provider key;鉴权、配额、fallback 和账号健康检查由 OmniRoute 承接。 +- 无额度、离线或 provider 失败时,任务进入 retry / failed,或降级为 `deterministic_only`;不得把未完成的 LLM graph build 标记为成功。 +- semantic extractor 和 reviewer 可分别覆盖模型:抽取默认便宜高速,reviewer 可在 spike 或显式 rebuild 时换成更高质量模型。 +- prompt、日志和 graph artifact 不记录 provider token、上传 URL 或完整外部响应,只记录 job id、model route、失败阶段和可审计摘要。 + +禁止: + +- 只输出无来源结论。 +- 把 LLM 推理结果当原文事实。 +- 在没有 locator 的情况下写入高置信 graph edge。 + +### 5.3 Graph Reviewer + +借鉴 Understand Anything 的 reviewer,但第一版不做大模型全量审查。先做三类检查: + +- 结构检查:node/edge 引用是否存在,locator JSON 是否可解析。 +- 证据检查:edge 的 `evidence_locator_json` 能否读回 quote。 +- 去重检查:alias / canonical_name 是否过度分裂。 + +LLM reviewer 只在 spike 阶段或用户显式重建时运行。 + +### 5.4 增量更新 + +增量粒度: + +```text +source file hash + -> artifact hash + -> section subtree hash + -> graph node/edge source_hash +``` + +文件变化时: + +- 原始文件 hash 未变:不重建。 +- artifact 变:只删除并重建该 source_hash 派生的 node/edge。 +- section 变:只重建对应 subtree。 +- graph schema version 变:排队重建当前 workspace graph。 + +## 6. Query Planner + +Planner 负责把用户问题变成 retrieval plan,而不是让 agent 直接猜关键词。 + +### 6.1 Query Types + +借鉴 BookRAG: + +- `simple`:单点事实,走 evidence search / node search。 +- `complex`:多个子问题,分别检索后合成。 +- `global`:列举、统计、总结、分类,走 tree / graph aggregation。 + +“羧酸/羧基的保护基有哪些,列举 5 种”应属于: + +```json +{ + "queryType": "global", + "operation": "LIST", + "targetConcept": "羧酸保护基", + "constraints": { + "limit": 5 + } +} +``` + +执行策略: + +1. term normalize:羧酸 / 羧基 / COOH / CO2H / carboxylic acid / carboxyl。 +2. graph search:找 concept node。 +3. relation traversal:`concept_has_subtype` / `concept_has_example`。 +4. evidence read:回读每个候选的 locator。 +5. answer synthesize:仅用回读证据组织答案。 + +### 6.2 Tool Contract + +新增 MNote tools: + +- `mnote.understanding.status` +- `mnote.understanding.rebuild` +- `mnote.understanding.cleanup_legacy` +- `mnote.understanding.jobs` +- `mnote.understanding.search_nodes` +- `mnote.understanding.traverse` +- `mnote.understanding.query` +- `mnote.understanding.read_evidence` +- `mnote.understanding.mindmap_projection` + +其中 `mnote.understanding.query` 返回: + +```json +{ + "ok": true, + "queryType": "global", + "operation": "LIST", + "items": [ + { + "name": "苄酯", + "nodeId": "concept:...", + "supportingEdges": ["edge:..."], + "evidence": [ + { + "quote": "...", + "locator": { "schema": "mnote.evidence_locator.v1" }, + "citationMarkdown": "[...](/documents/...)" + } + ], + "confidence": 0.82 + } + ], + "diagnostics": { + "planner": "understanding_graph", + "fallbackUsed": false + } +} +``` + +## 7. Understanding Dashboard 与任务面板 + +2026-06-06 路线修正:官方 Understand Anything dashboard 的价值不是“mindmap”,而是完整的 graph explorer:图布局、节点/边类型过滤、layer/community、搜索高亮、节点详情、guided tour、path finder、export、schema warning 和源码/证据面板。当前 MNote simplemindmap 不应继续作为 7-49 主 UI 承载。 + +第一阶段改为 **官方 UA dashboard 走通优先**: + +- 暂缓 `understanding_mindmap_projection` 的产品化 UI 工作。 +- 先安装/运行官方 UA dashboard,用 UA 原生 `knowledge-graph.json` 或 MNote 转换出的 UA-compatible graph 检查体验。 +- 若 dashboard 效果符合预期,再决定是 vendor/fork UA dashboard 组件,还是在 MNote Web 中实现同等的 `Understanding Dashboard`。 +- mindmap 只保留为后续轻量能力:answer-level 子图、导出、插入文档旁边;不再要求覆盖完整 Understanding Graph Explorer。 + +### 7.1 Official UA Dashboard Spike + +第一阶段目标: + +```text +MNote understanding graph / evidence graph + -> UA-compatible knowledge-graph.json + -> official Understand Anything dashboard + -> browser-visible graph explorer spike +``` + +验收: + +- dashboard 能打开并渲染图,不依赖 MNote mindmap。 +- 至少能展示 document / section / concept / claim / evidence source 节点。 +- 支持搜索、节点点击、相邻关系高亮、类型过滤或 layer/legend 中至少两类交互。 +- 节点详情能看到 summary、created_by/confidence、source artifact、EvidenceLocator 摘要。 +- 能从节点或证据 panel 回到 MNote evidence/open action,或者明确记录第一阶段暂缺的 deep-link 差距。 + +边界: + +- 官方 dashboard 只是体验 spike,不是 MNote 的长期事实源。 +- 不把 `.understand-anything/knowledge-graph.json` 当成 MNote graph 真相;真相仍在 `understanding.sqlite` / evidence store。 +- 不绕过 MNote workspace/root 权限直接读用户文件。 +- 不把 UA dashboard 的代码库 schema 原样扩展成 MNote 长期 schema;需要通过 adapter 投影。 + +### 7.2 Mindmap Projection(暂缓产品化) + +保留只读派生 projection,但不作为第一阶段主 UI: + +```text +understanding graph node/edge + -> understanding_mindmap_projection + -> MNote mindmap view +``` + +原则: + +- mindmap view 是派生展示,不是 graph 真相,也不是用户手工 mindmap 资源真相。 +- 真相仍在 `understanding.sqlite` 的 node / edge / evidence locator 表中。 +- projection 可按 document / section / concept / query result 生成,支持刷新和重建。 +- 节点点击后展示 summary、created_by、confidence、source artifact、evidence quote 和 locator open action。 +- 边点击后展示 relation_label、edge_kind、支持证据和 reviewer 状态。 +- 支持过滤 `deterministic | llm | reviewer | user` 来源,避免把 LLM 抽取和确定性结构混在一起。 + +后续 UI surface: + +- graph query 答案页可打开“查看理解图”,只展示本次答案涉及的 node / edge / evidence 子图。 +- 文档页资源 tab 可提供 `导出为 mindmap` 或 `插入子图`。 +- 不再把完整 workspace understanding graph 塞进 simplemindmap。 + +### 7.3 后台任务面板 + +Understanding Graph 属于异步派生数据系统,必须有后台任务可见性。后台面板至少展示: + +- job kind:`resource_parse`、`local_ocr`、`evidence_index_refresh`、`understanding_graph_build`、`understanding_graph_review`、`understanding_mindmap_projection_refresh`。 +- job state:`queued | running | retrying | failed | done | stale`。 +- 输入范围:workspace、root、document、section subtree、source artifact。 +- LLM runtime:provider、combo、model route、purpose、retry count。 +- 产出统计:artifact count、node count、edge count、locator count、mindmap node count。 +- 质量指标:locator 回读成功率、抽取 recall / precision、reviewer 发现的问题类型。 +- 失败诊断:OCR 噪声、parser 失败、OmniRoute 无额度、LLM JSON schema invalid、locator 断裂、schema version mismatch。 +- 操作:retry、rebuild selected scope、mark stale、open evidence、open mindmap projection。 + +边界: + +- 后台任务面板只读展示 job 账本和触发显式 rebuild,不允许绕过权限直接读 SQLite。 +- LLM prompt、provider token、上传 URL 和完整外部响应不在 UI 展示。 +- projection refresh 不能通过轮询长期驱动,优先由 ResourceWorkJob 状态变化和 realtime event 推送触发。 + +## 8. 与现有 7-46 / 7-48 的关系 + +### 8.1 7-46 Evidence Kernel + +`7-46` 仍是 quote / locator / open action 的基础。但其文件布局需要直接切到新存储: + +- `source-map.json` 不再写入 `{pageStem}.ocr/`。 +- `evidence.sqlite` 不再写入用户 workspace 的 `.mnote/index/`。 +- 旧 `{pageStem}.ocr/` 和 `.mnote/index/` 不作为 runtime fallback,不新增 legacy reader。 + +### 8.2 7-48 Resource Work Kernel + +`7-48` 的 `ResourceWorkJob` 应扩展为派生数据任务账本: + +- `local_ocr` +- `resource_parse` +- `evidence_index_refresh` +- `understanding_graph_build` +- `understanding_graph_review` +- `understanding_mindmap_projection_refresh` +- `workspace_sanity_check` + +任务结果应记录 artifact id、node/edge count、locator count、失败阶段和 rebuild reason。 + +## 9. 旧派生数据清理 + +当前阶段所有既有 OCR / index 都视为测试派生数据。为降低实现复杂度,不做旧格式兼容、导入器或双写。 + +### 9.1 清理范围 + +可清理对象: + +- 旧 `{pageStem}.ocr/` 目录。 +- 旧 `.mnote/index/` 目录。 +- 旧 `search-index.json`。 +- 旧 `evidence.sqlite`。 +- 旧 source-map / bbox / OCR provider 结果文件。 + +不可清理对象: + +- 用户 Markdown 正文。 +- 用户附件、PDF、Word、图片原始文件。 +- 用户显式创建的 mindmap / office / resource 文件。 +- 用户显式导出的 OCR 文本。 + +### 9.2 清理方式 + +新增一次性 cleanup job: + +- job kind:`legacy_derived_data_cleanup`。 +- 默认只扫描当前 workspace root 下的已知派生路径。 +- 清理前记录删除清单、文件数量、总字节数和 root。 +- 清理后强制触发 `resource_parse`、`evidence_index_refresh`、`understanding_graph_build`。 +- 清理失败不得影响用户原始文件打开,只标记派生数据 stale。 + +运行时口径: + +- 新 search / evidence / understanding routes 只读 control-plane index store。 +- watcher 不再监听旧 `.mnote/index` 写入。 +- UI 不再展示旧 OCR sidecar 作为系统文件;如果用户目录中仍残留,只按普通文件显示或由 cleanup job 删除。 +- status API 只返回 `storageLocation: "control_plane"`,不保留 `workspace_legacy` 状态。 + +## 10. 权限与隐私 + +- 派生 artifact 和 graph 必须继承原始 root / workspace / grant 权限。 +- 用户失去 root 访问权限后,不得再从 graph 查询到该 root 的 node/edge。 +- share workspace 下,graph query 必须按 user / workspace / rootUri 过滤。 +- OCR provider token、上传 URL、provider 原始响应不进入 graph。 +- graph node/edge 只存必要 quote / summary;完整 artifact 读取必须走权限检查。 +- 派生数据删除策略应跟随 workspace disconnect / revoke / explicit cleanup。 +- mindmap projection 必须继承 graph query 的权限过滤,不得展示用户无权访问的 node / edge。 + +## 11. 第一阶段 Spike + +目标不是做完整产品,而是证明 GraphRAG 层真的解决 FTS 失败的系统性问题。 + +### 11.1 输入 + +- 使用当前测试账号已有保护基书资源。 +- 读取现有 evidence / OCR / parse artifact。 +- 不写用户目录。 + +### 11.2 实现范围 + +1. 建一个最小 `understanding.sqlite`。 +2. 从 evidence blocks 生成 section tree。 +3. 对一个章节范围做 LLM 语义抽取: + - concept + - subtype/example + - definition/claim + - locator +4. 写入 node/edge。 +5. 实现一个 CLI 或 route: + - 输入问题:“羧酸/羧基的保护基有哪些,列举 5 种” + - 输出 graph-backed items + citation。 +6. 输出本次 spike 的 UA-compatible dashboard graph: + - 至少包含目标 concept、候选 subtype/example、supporting edge、evidence locator/source 节点。 + - 能在官方 UA dashboard 中打开并完成搜索、节点点击、过滤/图例等基础交互。 +7. 后台任务面板能看到 build / extraction / dashboard projection refresh 的状态和失败原因。 + +### 11.3 验收 + +- 不能只靠 FTS topK 直接回答。 +- 每个条目必须有 node/edge 和 EvidenceLocator。 +- 每个 EvidenceLocator 必须能用 `mnote.evidence.read/open` 回读或打开。 +- dashboard graph 必须能解释答案来源:答案条目 -> graph edge -> EvidenceLocator。 +- job 面板必须能看到 OmniRoute route、node/edge count、locator 回读率和失败阶段。 +- 手标至少 30 个候选实体,抽取 recall >= 70% 才继续。 +- precision 低于 70% 时必须记录错误类型:OCR 噪声、术语归一失败、LLM 幻觉、章节边界错误、关系 schema 不足。 +- 增量更新单个 source artifact 后,不触发整库 full rebuild。 + +## 12. 非目标 + +- 不把 Understand Anything、BookRAG、PageIndex、Kwipu 整包引入 MNote runtime。 +- 不把官方 UA dashboard 当成 MNote 长期事实源;第一阶段只作为体验 spike / adapter 验证。 +- 不新增默认常驻 graph 服务。 +- 不把 graph DB 当正文真相。 +- 不把 understanding dashboard / mindmap projection 当用户手工 mindmap 真相。 +- 不让 agent 直接读 SQLite 文件绕过 tool / permission。 +- 不把 OCR 文本默认插入用户 Markdown。 +- 不兼容旧 OCR sidecar 或 `.mnote/index` 作为 runtime fallback。 + +## 13. 后续实施顺序 + +2026-06-06 复核口径:mempalace 中能命中 7-49 前期只读勘察与 sqlite-graphrag 评估,但未找到当前 `7-49-local-understanding-graphrag-kernel-v1.md` 的完成记录;另有 2026-06-01 旧 `7-49 external audit / OpenClaw provider tests` 日志,属于同编号历史碰撞,不能作为本文完成证据。以下勾选只按当前实际代码确认。 + +- [x] 冻结派生数据新存储口径:control-plane artifact store + control-plane index store。 + - 证据:`local_search_index.rs` 的 `local_index_storage_dir()` 默认写入 `/mnt/Data1T/Mnote_data/control-plane/indexes/{workspace_hash}`,`evidence_sqlite_path()` / `understanding_sqlite_path()` 均落在该目录;`evidence.sqlite` 已新增 `derived_artifacts` 表存 parse/OCR/source-map 派生文本;status 返回 `storageLocation: "control_plane"` 与 `derivedArtifactCount`。 +- [ ] 给 7-46 直接切到 control-plane storage,删除 workspace legacy writer / reader 分支。 + - 复核:evidence/search index 主路径已切 control-plane index store;但 OCR sidecar、source-map/旧 sidecar 读取和 7-46 全链路仍未完全收口,不能勾选。 +- [x] 新增 `understanding.sqlite` 最小 schema。 + - 证据:`routes/understanding.rs` 已创建 `understanding_meta`、`understanding_node`、`understanding_edge`,并通过 `understanding_sqlite_path()` 持久化。 +- [x] 实现 `legacy_derived_data_cleanup` 与 `mnote.understanding.cleanup_legacy`,显式清理旧 OCR sidecar / `.mnote/index` 测试派生数据。 + - 证据:`local_search_index::legacy_derived_data_cleanup()` 扫描 `.mnote/index` 与 `*.ocr` 目录;`routes/understanding::cleanup_legacy()` 要求显式 `dryRun`,写操作走 workspace write access,清理后刷新 evidence index 并尝试重建 understanding graph。 +- [x] 实现 section tree builder。 + - 证据:`rebuild_understanding_graph()` 从 `evidence_block.section_path_json` 生成 `section` node 和 `document_contains_section` edge,并生成 `claim` node / `section_contains_claim` edge。当前只是最小 section/claim tree,不含 page/table/list 完整结构。 +- [x] 增加 Understanding Graph LLM runtime 配置:默认 OmniRoute combo `DeepSeek-v4-flash`,支持 extractor / reviewer 分别覆盖。 + - 证据:`default_llm_runtime()` 已输出 `semanticExtractor` / `graphReviewer` 两套配置;支持 `MNOTE_UNDERSTANDING_EXTRACTOR_*` 与 `MNOTE_UNDERSTANDING_REVIEWER_*` 环境变量分别覆盖,reviewer 默认关闭并保留 `deterministic_only` fallback。 +- [ ] 实现最小 LLM semantic extractor + reviewer。 + - 复核:当前抽取是 `deterministic_term_extractor` / `deterministic_protecting_group_normalizer`,没有实际 LLM 调用、JSON schema 校验或 reviewer。 +- [x] 实现 `mnote.understanding.query`。 + - 证据:HTTP route、Hermes tool manifest、tool dispatch 均已注册;查询先读 `understanding.sqlite` 的 concept edge,再 fallback evidence graph / sqlite search。 +- [ ] 安装并走通官方 UA dashboard spike,暂缓 MNote mindmap 产品化适配。 + - 复核:已确认官方 dashboard 功能面更接近 7-49 需求;但尚未把 MNote graph 转成 UA-compatible `knowledge-graph.json` 并真实打开 dashboard。 +- [ ] 增加后台 Understanding job 面板,展示 build / review / dashboard projection refresh 状态。 + - 复核:`mnote.understanding.jobs` 已有合成 jobs API/tool,并返回 review、locator parse success rate 与失败阶段;但没有后台 UI 面板,也未接真实 ResourceWorkJob 账本。 +- [ ] 跑保护基书 spike,并记录 recall / precision / locator 回跳率和 mindmap 可解释性。 + - 复核:代码里有最小单元测试覆盖羧酸保护基 query variant 和 term extraction;未见真实保护基书 spike、人工标注 30 个实体、recall/precision 记录或浏览器验证证据。 +- [ ] 若 spike 通过,再扩到多文档 workspace 和 UI。 + - 复核:当前仍是单 workspace/root 的最小 kernel/tool 路径,未进入多文档 UI 产品化。 + +## 14. 2026-06-06 实际代码完成情况复核 + +### 14.1 已完成的最小能力 + +- [x] control-plane index store:`search-index.json`、`evidence.sqlite`、`understanding.sqlite` 默认位于 control-plane 管理目录,不写入用户 workspace 的旧 `.mnote/index`。 +- [x] control-plane artifact table:`evidence.sqlite.derived_artifacts` 已存 `parse_text` / `ocr_text` / `source_map`,带 workspace/root/source hash/content hash/stale 字段;`local_index.status` 返回 `derivedArtifactCount`。 +- [x] Understanding HTTP routes:`/api/understanding/status|rebuild|cleanup_legacy|jobs|search_nodes|traverse|query|read_evidence|mindmap_projection` 已注册。 +- [x] Hermes tools:`mnote.understanding.*` 九个工具已在 manifest、dispatch、skill allowlist 中注册。 +- [x] 最小 deterministic graph builder:从 `evidence_resource` / `evidence_block` 生成 document/resource、section、claim、concept node 与 contains/support/example edge。 +- [x] EvidenceLocator 回读桥:`mnote.understanding.read_evidence` 复用 `mnote.evidence.read` 的 read payload。 +- [x] 旧派生数据显式 cleanup:支持 dry-run plan 和 apply,范围限定旧 `.mnote/index` 与 `*.ocr` sidecar。 +- [x] LLM runtime 配置面:默认 OmniRoute combo `DeepSeek-v4-flash`,extractor / reviewer 可分别通过环境变量覆盖。 +- [x] 确定性 graph reviewer:重建后记录 `last_review_json`,检查 edge node 引用、locator JSON parse、canonical name 重复,并在 jobs/status 返回 locator parse success rate 与失败阶段。 +- [x] mindmap projection 证据链增强:projection 节点 metadata 已包含 `supportingEdges` / `supportingEdgeKinds` / `evidence` / `confidence` / `createdBy`;该能力降级为 answer-level 辅助,不再作为主 UI。 +- [x] 最小测试:`understanding_rebuild_query_and_mindmap_use_control_plane_store`、`legacy_cleanup_removes_only_derived_sidecars`、query variant 和 term extraction 单测已存在。 + +### 14.2 未完成或只能算部分完成 + +- [ ] artifact runtime cutover:已有 `derived_artifacts` 表和 parse/OCR/source-map 写入;但 parse/OCR runtime 仍会写 legacy sidecar 文件,尚未改成只依赖 artifact store/blob store。 +- [ ] 7-46 evidence kernel 全面切流:evidence/search index 路径已切,但旧 OCR sidecar / source-map / fallback 边界未完全删除。 +- [ ] LLM semantic extractor:没有调用 OmniRoute / DeepSeek-v4-flash,也没有结构化 LLM graph artifact。 +- [ ] LLM Graph reviewer:确定性结构/locator/去重检查已落地;但还没有显式 rebuild 时可启用的 LLM reviewer。 +- [ ] 增量更新:`understanding.rebuild` 仍是按当前 evidence 全量重建;没有 source artifact / section subtree 粒度的 node/edge 删除重建。 +- [ ] 后台任务面板:无 UI surface;jobs API 已返回 review/locator 指标,但仍未接真实 ResourceWorkJob 账本。 +- [ ] 官方 UA dashboard spike:尚未安装/启动 dashboard,也没有 MNote -> UA-compatible graph adapter。 +- [ ] 资源 tab / workspace `Understanding Dashboard`:当前没有浏览器 runtime / SSR UI 入口。 +- [ ] 保护基书 spike 验收:没有真实资源验证、locator 回跳率、recall/precision 或截图/浏览器证据。 + +### 14.3 当前归档判断 + +本文不能移动到 `done`。当前实际完成的是 `Local Understanding GraphRAG Kernel` 的最小 deterministic kernel/tool spike;产品化条件仍卡在 artifact store、LLM extractor/reviewer、任务账本/UI、真实保护基书 spike 和增量更新。 diff --git a/rust/crates/mnote-web/browser/document-editor-adapter-runtime.js b/rust/crates/mnote-web/browser/document-editor-adapter-runtime.js index 5785559b..662b93dc 100644 --- a/rust/crates/mnote-web/browser/document-editor-adapter-runtime.js +++ b/rust/crates/mnote-web/browser/document-editor-adapter-runtime.js @@ -258,6 +258,26 @@ import { if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(text); return text.replace(/["\\]/g, '\\$&'); }; + const localFileOpenUrl = (rootUri, path) => { + const normalizedRootUri = String(rootUri || '').trim(); + const normalizedPath = String(path || '').trim(); + if (!normalizedRootUri || !normalizedPath) return ''; + const url = new URL('/api/local-folder/files/open', window.location.origin); + url.searchParams.set('rootUri', normalizedRootUri); + url.searchParams.set('path', normalizedPath); + return url.toString(); + }; + const resourceHrefFromUrlState = (rootUri, path, title) => { + const fileUrl = localFileOpenUrl(rootUri, path); + if (!fileUrl) return ''; + if (/\.pdf$/i.test(String(title || path || ''))) { + const url = new URL('/pdf-preview', window.location.origin); + url.searchParams.set('fileUrl', fileUrl); + url.searchParams.set('fileName', title || path); + return url.toString(); + } + return fileUrl; + }; const applyDocumentEvidenceLocatorFromUrl = (root) => { if (!(root instanceof HTMLElement)) return; const url = currentUrl(); @@ -289,7 +309,7 @@ import { const url = currentUrl(); const raw = String(url.searchParams.get('resourceTab') || '').trim(); if (!raw) return null; - const objectIdentity = raw.includes('::') ? raw.slice(raw.indexOf('::') + 2) : raw; + const objectIdentity = raw.startsWith('resource:') ? raw : (raw.includes('::') ? raw.slice(raw.indexOf('::') + 2) : raw); if (!objectIdentity.startsWith('resource:file:')) return null; const rootUri = currentWebShellRootUri(); if (!rootUri) return null; @@ -299,6 +319,7 @@ import { const path = rest.slice(prefix.length).replace(/^\/+/, ''); if (!path) return null; const title = path.split('/').filter(Boolean).pop() || path; + const href = resourceHrefFromUrlState(rootUri, path, title); return { objectIdentity, documentId: currentWebShellDocumentId(), @@ -309,6 +330,8 @@ import { assetId: `local:asset:${path}`, title, fileName: title, + href, + officeUrl: href, openTarget: 'active-tab', page: url.searchParams.get('page') || undefined, bbox: url.searchParams.get('bbox') || undefined, @@ -1021,18 +1044,22 @@ import { }); }, { once: true }); + const restoreResourceTabFromUrl = () => { + const restoreInput = restoreResourceTabInputFromUrl(); + if (!restoreInput) return Promise.resolve(false); + return openResourceInActiveTab(restoreInput).catch((error) => { + console.warn('mnote resource tab URL 恢复失败', error); + return false; + }); + }; + if (paneRuntimes.length) { Promise.all(paneRuntimes.map((paneRuntime) => mountPane(paneRuntime))).then(() => { - const restoreInput = restoreResourceTabInputFromUrl(); - if (restoreInput) { - return openResourceInActiveTab(restoreInput).catch((error) => { - console.warn('mnote resource tab URL 恢复失败', error); - return false; - }); - } - return false; + return restoreResourceTabFromUrl(); }).catch((error) => { console.error('mnote multi-pane editor mount failed', error); }); + } else { + void restoreResourceTabFromUrl(); } })(); diff --git a/rust/crates/mnote-web/browser/document-resource-tab-runtime.js b/rust/crates/mnote-web/browser/document-resource-tab-runtime.js index 25ad3081..aaf72017 100644 --- a/rust/crates/mnote-web/browser/document-resource-tab-runtime.js +++ b/rust/crates/mnote-web/browser/document-resource-tab-runtime.js @@ -1373,17 +1373,17 @@ export const createResourceTabRuntime = (dependencies = {}) => { const statusNode = entry.panel.querySelector('[data-mnote-local-ocr-status-text]'); if (statusNode instanceof HTMLElement) { statusNode.textContent = message || ( - normalizedStatus === 'done' ? 'OCR 已完成' - : normalizedStatus === 'running' ? 'OCR 处理中' - : normalizedStatus === 'failed' ? 'OCR 失败' - : normalizedStatus === 'stale' ? 'OCR 需更新' - : 'OCR 未生成' + normalizedStatus === 'done' ? '资料库已索引' + : normalizedStatus === 'running' ? '资料库索引中' + : normalizedStatus === 'failed' ? '资料库索引失败' + : normalizedStatus === 'stale' ? '资料库需更新' + : '资料库未索引' ); } const openButton = entry.panel.querySelector('[data-mnote-local-ocr-action="open"]'); - if (openButton instanceof HTMLButtonElement) openButton.disabled = !entry.localOcrJob?.ocrRootRelativePath; + if (openButton instanceof HTMLButtonElement) openButton.disabled = true; const insertButton = entry.panel.querySelector('[data-mnote-local-ocr-action="insert"]'); - if (insertButton instanceof HTMLButtonElement) insertButton.disabled = !entry.localOcrJob?.ocrRootRelativePath; + if (insertButton instanceof HTMLButtonElement) insertButton.disabled = true; window.dispatchEvent(new CustomEvent('mnote:local-ocr-job-updated', { detail: { status: normalizedStatus, @@ -1394,15 +1394,50 @@ export const createResourceTabRuntime = (dependencies = {}) => { })); }; + const knowledgeRagJobFromRegistryEntry = (registryEntry, rootUri) => { + if (!registryEntry || typeof registryEntry !== 'object') return null; + const sourceRootRelativePath = String(registryEntry.sourceRootRelativePath || registryEntry.source_path || '').trim(); + if (!sourceRootRelativePath) return null; + const indexed = Boolean(registryEntry.indexedAtMs || registryEntry.indexed_at_ms) && Boolean(registryEntry.lightRagDocId || registryEntry.light_rag_doc_id); + const failed = registryEntry.deletedAtMs != null || registryEntry.deleted_at_ms != null || registryEntry.stale === true; + const status = failed ? 'failed' : indexed ? 'done' : 'running'; + return { + taskKind: 'knowledge_rag', + jobId: String(registryEntry.sourceId || registryEntry.source_id || `knowledge-rag:${sourceRootRelativePath}`), + sourceRootRelativePath, + rootUri: String(registryEntry.rootUri || rootUri || '').trim(), + ocrRootRelativePath: '', + provider: 'lightrag', + status, + stageLabel: status === 'done' ? '资料库已索引' : status === 'failed' ? '资料库需重建' : '资料库索引中', + stale: registryEntry.stale === true, + updatedAtMs: Number(registryEntry.updatedAtMs || registryEntry.updated_at_ms || Date.now()), + finishedAtMs: status === 'done' ? Number(registryEntry.indexedAtMs || registryEntry.indexed_at_ms || Date.now()) : null, + error: status === 'failed' ? 'knowledge_rag_source_stale_or_deleted' : '', + lightRagDocId: String(registryEntry.lightRagDocId || registryEntry.light_rag_doc_id || ''), + lightRagFilePath: String(registryEntry.lightRagFilePath || registryEntry.light_rag_file_path || ''), + }; + }; + + const knowledgeRagRegistryEntries = (payload) => { + const registry = payload && payload.registry && typeof payload.registry === 'object' ? payload.registry : null; + return Array.isArray(registry?.entries) ? registry.entries : []; + }; + const readLocalOcrStatus = async (entry) => { if (!isLocalOcrSourceEntry(entry)) return null; - const url = new URL('/api/local-folder/ocr/status', window.location.origin); + const url = new URL('/api/knowledge-rag/status', window.location.origin); url.searchParams.set('rootUri', entry.rootUri); - url.searchParams.set('sourceRootRelativePath', entry.path); + const workspaceId = String(entry.workspaceId || currentWebShellWorkspaceId() || '').trim(); + if (workspaceId) url.searchParams.set('workspaceId', workspaceId); const response = await fetch(url.toString(), { cache: 'no-store', headers: { accept: 'application/json' } }); const payload = await response.json().catch(() => null); if (!response.ok || !payload || payload.ok !== true) return null; - return payload.job || null; + const targetPath = String(entry.path || '').trim().replace(/\\/g, '/').replace(/^\/+/, ''); + const registryEntry = knowledgeRagRegistryEntries(payload).find((candidate) => { + return String(candidate?.sourceRootRelativePath || '').trim().replace(/\\/g, '/').replace(/^\/+/, '') === targetPath; + }); + return knowledgeRagJobFromRegistryEntry(registryEntry, entry.rootUri); }; const localOcrTaskState = { @@ -1475,7 +1510,7 @@ export const createResourceTabRuntime = (dependencies = {}) => { toggle.addEventListener('click', (event) => { if (toggle.getAttribute('data-mnote-action') === 'open-ocr-settings') { event.preventDefault(); - window.dispatchEvent(new CustomEvent('mnote:open-local-ocr-settings')); + window.dispatchEvent(new CustomEvent('mnote:open-knowledge-rag-settings')); return; } void runManualLocalOcrForActiveTarget(toggle).catch((error) => { @@ -1506,7 +1541,7 @@ export const createResourceTabRuntime = (dependencies = {}) => { const statusTextForLocalOcrJob = (job) => { const status = String(job?.status || '').trim(); if (job?.stageLabel) return String(job.stageLabel); - if (job?.taskKind === 'local_index') { + if (job?.taskKind === 'local_index' || job?.taskKind === 'knowledge_rag') { return status === 'done' ? '索引已完成' : status === 'failed' ? '索引失败' : status === 'running' ? '索引中' @@ -1557,20 +1592,7 @@ export const createResourceTabRuntime = (dependencies = {}) => { document.body.appendChild(dock); } let toggle = document.querySelector('[data-testid="mnote-local-ocr-task-toggle"]'); - if (!(toggle instanceof HTMLButtonElement)) { - const actions = document.querySelector('.wolai-topbar-actions'); - toggle = document.createElement('button'); - toggle.type = 'button'; - toggle.className = 'wolai-icon-button mnote-local-ocr-task-toggle'; - toggle.setAttribute('title', 'OCR 设置'); - toggle.setAttribute('aria-label', 'OCR 设置'); - toggle.setAttribute('data-testid', 'mnote-local-ocr-task-toggle'); - toggle.setAttribute('data-mnote-action', 'open-ocr-settings'); - toggle.innerHTML = ''; - if (actions instanceof HTMLElement) actions.appendChild(toggle); - else document.body.appendChild(toggle); - } - bindLocalOcrTopbarAction(); + if (toggle instanceof HTMLButtonElement) bindLocalOcrTopbarAction(); if (dock.getAttribute('data-mnote-local-ocr-bound') === 'true') return dock; dock.setAttribute('data-mnote-local-ocr-bound', 'true'); dock.addEventListener('click', (event) => { @@ -1617,23 +1639,7 @@ export const createResourceTabRuntime = (dependencies = {}) => { } const openButton = event.target instanceof Element ? event.target.closest('[data-mnote-local-ocr-task-open]') : null; if (openButton instanceof HTMLElement) { - const sourcePath = openButton.getAttribute('data-mnote-local-ocr-task-open') || ''; - const job = localOcrTaskState.jobsBySource.get(sourcePath) || null; - if (job) { - void openResourceInActiveTab({ - kind: 'markdown', - title: String(job.ocrRootRelativePath || '').split('/').filter(Boolean).pop() || 'OCR', - path: job.ocrRootRelativePath, - objectIdentity: `local-ocr:${job.ocrRootRelativePath}`, - assetId: `local-ocr:${job.ocrRootRelativePath}`, - documentId: job.ownerDocumentId || currentWebShellDocumentId() || '', - ownerDocumentId: job.ownerDocumentId || currentWebShellDocumentId() || '', - workspaceId: currentWebShellWorkspaceId() || '', - sourceKind: 'local_folder', - rootUri: localOcrTaskState.rootUri || '', - resourceKind: 'markdown', - }).catch((error) => console.warn('mnote local OCR 任务打开失败', error)); - } + window.dispatchEvent(new CustomEvent('mnote:open-knowledge-rag-settings')); } const retryButton = event.target instanceof Element ? event.target.closest('[data-mnote-local-ocr-task-retry]') : null; if (retryButton instanceof HTMLElement) { @@ -1670,7 +1676,7 @@ export const createResourceTabRuntime = (dependencies = {}) => { .filter((node) => node instanceof HTMLButtonElement); taskToggles.forEach((toggle) => { const opensSettings = toggle.getAttribute('data-mnote-action') === 'open-ocr-settings'; - const label = runningCount > 0 ? `${runningCount} 个 OCR 处理中,打开 OCR 设置` : (jobs.length > 0 ? `${jobs.length} 个 OCR 任务,打开 OCR 设置` : 'OCR 设置'); + const label = runningCount > 0 ? `${runningCount} 个资料库索引处理中,打开资料库设置` : (jobs.length > 0 ? `${jobs.length} 个资料库任务,打开资料库设置` : '资料库设置'); const taskLabel = runningCount > 0 ? `${runningCount} 个后台任务正在运行` : (jobs.length > 0 ? `${jobs.length} 个后台任务` : '后台任务'); toggle.setAttribute('title', opensSettings ? label : taskLabel); toggle.setAttribute('aria-label', opensSettings ? label : taskLabel); @@ -1681,7 +1687,7 @@ export const createResourceTabRuntime = (dependencies = {}) => { badge.textContent = String(runningCount > 0 ? runningCount : jobs.length); badge.hidden = jobs.length === 0; } else { - toggle.textContent = runningCount > 0 ? `OCR ${runningCount} 处理中` : `OCR ${jobs.length}`; + toggle.textContent = runningCount > 0 ? `索引 ${runningCount} 处理中` : `索引 ${jobs.length}`; } }); const drawer = dock.querySelector('[data-testid="mnote-local-ocr-task-drawer"]'); @@ -1731,7 +1737,7 @@ export const createResourceTabRuntime = (dependencies = {}) => { row.setAttribute('data-mnote-local-ocr-task-row', String(job.sourceRootRelativePath || '')); row.setAttribute('data-mnote-local-ocr-task-status', String(job.status || '')); row.setAttribute('data-mnote-local-ocr-task-category', localOcrTaskCategory(job)); - const title = String(job.sourceRootRelativePath || '').split('/').filter(Boolean).pop() || 'OCR'; + const title = String(job.sourceRootRelativePath || '').split('/').filter(Boolean).pop() || '资料库索引'; const category = localOcrTaskCategory(job); const progress = localOcrTaskProgress(job); row.innerHTML = '
'; @@ -1766,7 +1772,7 @@ export const createResourceTabRuntime = (dependencies = {}) => { if (open instanceof HTMLButtonElement) { open.setAttribute('data-mnote-local-ocr-task-open', String(job.sourceRootRelativePath || '')); open.disabled = !job.ocrRootRelativePath; - open.hidden = job.taskKind === 'local_index'; + open.hidden = job.taskKind === 'local_index' || job.taskKind === 'knowledge_rag'; } const retry = row.querySelector('[data-mnote-local-ocr-task-retry]'); if (retry instanceof HTMLButtonElement) { @@ -1780,7 +1786,7 @@ export const createResourceTabRuntime = (dependencies = {}) => { const deleteOcr = row.querySelector('[data-mnote-local-ocr-task-delete]'); if (deleteOcr instanceof HTMLButtonElement) { deleteOcr.setAttribute('data-mnote-local-ocr-task-delete', String(job.sourceRootRelativePath || '')); - deleteOcr.hidden = job.taskKind === 'local_index' || !job.ocrRootRelativePath; + deleteOcr.hidden = job.taskKind === 'local_index' || !String(job.sourceRootRelativePath || '').trim(); } list.appendChild(row); }); @@ -1790,13 +1796,15 @@ export const createResourceTabRuntime = (dependencies = {}) => { const normalizedRoot = String(rootUri || '').trim(); if (!normalizedRoot) return; localOcrTaskState.rootUri = normalizedRoot; - const url = new URL('/api/local-folder/ocr/jobs', window.location.origin); + const url = new URL('/api/knowledge-rag/status', window.location.origin); url.searchParams.set('rootUri', normalizedRoot); + const workspaceId = String(currentWebShellWorkspaceId() || '').trim(); + if (workspaceId) url.searchParams.set('workspaceId', workspaceId); const response = await fetch(url.toString(), { cache: 'no-store', headers: { accept: 'application/json' } }); const payload = await response.json().catch(() => null); if (!response.ok || !payload || payload.ok !== true) return; - (Array.isArray(payload.jobs) ? payload.jobs : []).forEach((job) => { - if (job && typeof job === 'object') job.rootUri = normalizedRoot; + knowledgeRagRegistryEntries(payload).forEach((entry) => { + const job = knowledgeRagJobFromRegistryEntry(entry, normalizedRoot); updateLocalOcrTaskState(job); }); renderLocalOcrTaskDock(); @@ -1827,74 +1835,37 @@ export const createResourceTabRuntime = (dependencies = {}) => { }; const openLocalOcrSidecar = async (entry, job) => { - const target = job || entry?.localOcrJob || null; - const ocrPath = String(target?.ocrRootRelativePath || '').trim(); - if (!ocrPath || typeof openResourceInActiveTab !== 'function') return false; - const title = ocrPath.split('/').filter(Boolean).pop() || 'OCR'; - return await openResourceInActiveTab({ - kind: 'markdown', - title, - path: ocrPath, - objectIdentity: `local-ocr:${ocrPath}`, - assetId: `local-ocr:${ocrPath}`, - documentId: String(entry.ownerDocumentId || entry.documentId || currentWebShellDocumentId() || '').trim(), - ownerDocumentId: String(entry.ownerDocumentId || entry.documentId || currentWebShellDocumentId() || '').trim(), - workspaceId: String(entry.workspaceId || currentWebShellWorkspaceId() || '').trim(), - sourceKind: 'local_folder', - rootUri: String(entry.rootUri || '').trim(), - resourceKind: 'markdown', - paneRole: normalizePaneRole(entry.paneRole), - }); + window.dispatchEvent(new CustomEvent('mnote:open-knowledge-rag-settings')); + return false; }; const insertLocalOcrLink = async (entry, job) => { - const target = job || entry?.localOcrJob || null; - const ocrPath = String(target?.ocrRootRelativePath || '').trim(); - if (!ocrPath) return false; - const response = await fetch('/api/local-folder/ocr/insert', { - method: 'POST', - headers: { 'content-type': 'application/json', accept: 'application/json' }, - body: JSON.stringify({ - rootUri: String(entry.rootUri || '').trim(), - documentId: String(entry.ownerDocumentId || entry.documentId || currentWebShellDocumentId() || '').trim(), - ocrRootRelativePath: ocrPath, - mode: 'link', - }), - }); - const payload = await response.json().catch(() => null); - if (!response.ok || !payload || payload.ok !== true) { - throw new Error(payload?.error?.message || `local_ocr_insert_failed_${response.status}`); - } - setLocalOcrStatus(entry, 'done', 'OCR 链接已插入正文', target); - return true; + throw new Error('OCR sidecar 已退役,请使用 LightRAG 资料库引用与问答。'); }; const createLocalOcrJob = async (entry) => { if (!isLocalOcrSourceEntry(entry)) return null; - const provider = localOcrProvider(); const entryRootUri = String(entry.rootUri || '').trim(); if (entryRootUri) localOcrTaskState.rootUri = entryRootUri; const startedAt = Date.now(); - const pendingJob = localOcrJobSnapshotFromEntry(entry, 'running', provider, '处理中', startedAt); - setLocalOcrStatus(entry, 'running', 'OCR 处理中', pendingJob); + const pendingJob = localOcrJobSnapshotFromEntry(entry, 'running', 'lightrag', '资料库索引中', startedAt); + pendingJob.taskKind = 'knowledge_rag'; + setLocalOcrStatus(entry, 'running', '资料库索引中', pendingJob); updateLocalOcrTaskState(pendingJob); + const workspaceId = String(entry.workspaceId || currentWebShellWorkspaceId() || '').trim(); const body = { + workspaceId, rootUri: String(entry.rootUri || '').trim(), - documentId: String(entry.ownerDocumentId || entry.documentId || currentWebShellDocumentId() || '').trim(), - sourceRootRelativePath: String(entry.path || '').trim(), - provider, + sources: [{ sourcePath: String(entry.path || '').trim() }], }; - if (provider === 'mock') { - body.mockMarkdown = `# OCR Result\n\n${entry.title || entry.path} OCR UI smoke text`; - } - const response = await fetch('/api/local-folder/ocr/jobs', { + const response = await fetch('/api/knowledge-rag/ingest', { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json' }, body: JSON.stringify(body), }); const payload = await response.json().catch(() => null); if (!response.ok || !payload || payload.ok !== true) { - const message = payload?.error?.message || payload?.message || payload?.error?.code || `local_ocr_job_failed_${response.status}`; + const message = payload?.error?.message || payload?.message || payload?.error?.code || `knowledge_rag_ingest_failed_${response.status}`; const failedJob = { ...pendingJob, status: 'failed', @@ -1907,10 +1878,22 @@ export const createResourceTabRuntime = (dependencies = {}) => { updateLocalOcrTaskState(failedJob); throw new Error(message); } - const job = payload.job && typeof payload.job === 'object' ? { ...payload.job, rootUri: entryRootUri } : null; + const registryEntry = knowledgeRagRegistryEntries(payload).find((candidate) => { + return String(candidate?.sourceRootRelativePath || '').trim().replace(/\\/g, '/').replace(/^\/+/, '') === String(entry.path || '').trim().replace(/\\/g, '/').replace(/^\/+/, ''); + }); + const job = knowledgeRagJobFromRegistryEntry(registryEntry, entryRootUri) || { + ...pendingJob, + status: payload.retryRequired ? 'running' : 'done', + stageLabel: payload.retryRequired ? 'LightRAG 忙碌,等待重试' : '资料库已提交索引', + updatedAtMs: Date.now(), + finishedAtMs: payload.retryRequired ? null : Date.now(), + }; const displayStatus = localOcrDisplayStatus(job, 'done'); - setLocalOcrStatus(entry, displayStatus, displayStatus === 'stale' ? 'OCR 需更新' : 'OCR 已完成', job); + setLocalOcrStatus(entry, displayStatus, displayStatus === 'stale' ? '资料库需更新' : statusTextForLocalOcrJob(job), job); if (job) updateLocalOcrTaskState(job); + window.dispatchEvent(new CustomEvent('mnote:knowledge-rag-source-updated', { + detail: { rootUri: entryRootUri, workspaceId, sourceRootRelativePath: String(entry.path || '').trim(), result: payload } + })); return job; }; @@ -1919,27 +1902,24 @@ export const createResourceTabRuntime = (dependencies = {}) => { if (!sourcePath) return false; const job = localOcrTaskState.jobsBySource.get(sourcePath) || null; const rootUri = String(job?.rootUri || localOcrTaskState.rootUri || currentWebShellRootUri() || '').trim(); - const response = await fetch('/api/local-folder/ocr/delete', { + const response = await fetch('/api/knowledge-rag/delete-source', { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json' }, body: JSON.stringify({ rootUri, - sourceRootRelativePath: sourcePath, + workspaceId: String(currentWebShellWorkspaceId() || '').trim(), + sourcePath, }), }); const payload = await response.json().catch(() => null); if (!response.ok || !payload || payload.ok !== true) { - throw new Error(payload?.error?.message || `local_ocr_delete_failed_${response.status}`); - } - if (payload.deleted !== true) { - throw new Error('local_ocr_delete_noop'); + throw new Error(payload?.error?.message || `knowledge_rag_delete_failed_${response.status}`); } localOcrTaskState.jobsBySource.delete(sourcePath); renderLocalOcrTaskDock(); - const ocrPath = String(job?.ocrRootRelativePath || '').trim(); - if (ocrPath) { - dispatchLocalOcrFileTreeRefresh({ ...job, status: 'stale', updatedAtMs: Date.now(), ocrRootRelativePath: ocrPath }, localOcrTaskState.rootUri); - } + window.dispatchEvent(new CustomEvent('mnote:knowledge-rag-source-updated', { + detail: { rootUri, workspaceId: String(currentWebShellWorkspaceId() || '').trim(), sourceRootRelativePath: sourcePath, result: payload } + })); return true; }; @@ -2118,14 +2098,14 @@ export const createResourceTabRuntime = (dependencies = {}) => { }); }); } - setLocalOcrStatus(entry, 'idle', 'OCR 未生成', null); + setLocalOcrStatus(entry, 'idle', '资料库未索引', null); ensureLocalOcrTaskDock(); ensureLocalOcrTaskEvents(entry.rootUri); void loadLocalOcrJobs(entry.rootUri).catch(() => undefined); void readLocalOcrStatus(entry).then((job) => { if (!job) return; const displayStatus = localOcrDisplayStatus(job, 'done'); - setLocalOcrStatus(entry, displayStatus, displayStatus === 'stale' ? 'OCR 需更新' : 'OCR 已完成', job); + setLocalOcrStatus(entry, displayStatus, displayStatus === 'stale' ? '资料库需更新' : statusTextForLocalOcrJob(job), job); updateLocalOcrTaskState(job); }).catch(() => undefined); }; diff --git a/rust/crates/mnote-web/browser/sidebar-filetree-command-runtime.js b/rust/crates/mnote-web/browser/sidebar-filetree-command-runtime.js index 766bc52a..7da67c14 100644 --- a/rust/crates/mnote-web/browser/sidebar-filetree-command-runtime.js +++ b/rust/crates/mnote-web/browser/sidebar-filetree-command-runtime.js @@ -421,70 +421,77 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => { } async function runLocalOcrForDetail(detail, trigger) { - var sourceRootRelativePath = localOcrSourceRelativePath(detail); - var rootUri = localOcrRootUri(detail, trigger); - var documentId = String(detail && detail.documentId || currentDocumentId() || '').trim(); - if (!sourceRootRelativePath || !rootUri || !documentId) { - throw new Error('缺少 OCR 来源、rootUri 或 owner documentId'); - } - if (!isLocalOcrSourceFileName(String(detail && (detail.title || detail.fileName) || sourceRootRelativePath))) { - throw new Error('OCR 仅支持图片和 PDF'); - } - var provider = localOcrProvider(); - var body = { - rootUri: rootUri, - documentId: documentId, - sourceRootRelativePath: sourceRootRelativePath, - provider: provider - }; - if (provider === 'mock') { - body.mockMarkdown = '# OCR Result\n\n' + (detail.title || sourceRootRelativePath) + ' OCR menu smoke text'; - } document.documentElement.setAttribute('data-mnote-local-ocr-menu-status', 'running'); - var response = await fetch('/api/local-folder/ocr/jobs', { + var payload = await ingestKnowledgeRagForDetail(detail, trigger).catch(function(error) { + document.documentElement.setAttribute('data-mnote-local-ocr-menu-status', 'failed'); + throw error; + }); + var sourceRootRelativePath = localOcrSourceRelativePath(detail) + || String(detail && (detail.localRelativePath || detail.path || detail.title) || '').trim(); + var rootUri = localOcrRootUri(detail, trigger); + var status = payload && payload.retryRequired ? 'retry' : 'done'; + document.documentElement.setAttribute('data-mnote-local-ocr-menu-status', status); + document.documentElement.setAttribute('data-mnote-local-ocr-menu-path', sourceRootRelativePath); + window.dispatchEvent(new CustomEvent('mnote:local-ocr-job-updated', { + detail: { status: status, job: payload, rootUri: rootUri, sourceRootRelativePath: sourceRootRelativePath } + })); + return payload; + } + + async function ingestKnowledgeRagForDetail(detail, trigger) { + var sourceRootRelativePath = localOcrSourceRelativePath(detail) + || String(detail && (detail.localRelativePath || detail.path || detail.title) || '').trim(); + var rootUri = localOcrRootUri(detail, trigger); + if (!sourceRootRelativePath || !rootUri) { + throw new Error('缺少资料库来源或 rootUri'); + } + var workspaceId = String(detail && detail.workspaceId || resolveWorkspaceId(document.body) || '').trim(); + document.documentElement.setAttribute('data-mnote-knowledge-rag-menu-status', 'running'); + document.documentElement.setAttribute('data-mnote-knowledge-rag-menu-path', sourceRootRelativePath); + var response = await fetch('/api/knowledge-rag/ingest', { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json' }, - body: JSON.stringify(body) + body: JSON.stringify({ + workspaceId: workspaceId, + rootUri: rootUri, + sources: [{ sourcePath: sourceRootRelativePath }] + }) }); var payload = await response.json().catch(function() { return null; }); - if (!response.ok || !payload || payload.ok !== true) { - var message = payload && payload.error && payload.error.message ? payload.error.message : 'OCR 生成失败'; - document.documentElement.setAttribute('data-mnote-local-ocr-menu-status', 'failed'); + if (!response.ok || !payload || payload.ok === false) { + var message = payload && payload.message ? payload.message : '资料库索引失败'; + document.documentElement.setAttribute('data-mnote-knowledge-rag-menu-status', 'failed'); throw new Error(message); } - var job = payload.job || {}; - document.documentElement.setAttribute('data-mnote-local-ocr-menu-status', String(job.status || 'done')); - if (job.ocrRootRelativePath) { - document.documentElement.setAttribute('data-mnote-local-ocr-menu-path', String(job.ocrRootRelativePath)); - if (typeof openLocalResourceInActiveTab === 'function') { - await openLocalResourceInActiveTab({ - path: String(job.ocrRootRelativePath), - title: String(job.ocrRootRelativePath).split('/').filter(Boolean).pop() || 'OCR', - kind: 'markdown', - assetId: 'local-ocr:' + String(job.ocrRootRelativePath), - documentId: documentId, - workspaceId: String(detail && detail.workspaceId || resolveWorkspaceId(document.body) || '').trim(), - rootUri: rootUri - }); - } - } - window.dispatchEvent(new CustomEvent('mnote:local-ocr-job-updated', { - detail: { status: String(job.status || 'done'), job: job, rootUri: rootUri, sourceRootRelativePath: sourceRootRelativePath } + document.documentElement.setAttribute('data-mnote-knowledge-rag-menu-status', payload.retryRequired ? 'retry' : 'done'); + window.dispatchEvent(new CustomEvent('mnote:knowledge-rag-source-updated', { + detail: { rootUri: rootUri, workspaceId: workspaceId, sourceRootRelativePath: sourceRootRelativePath, result: payload } })); - return job; + return payload; } function handleTreeContextMenuAction(action, detail, trigger) { closeTreeContextMenu(); detail = detail || {}; if (action === 'local-ocr') { - recordFileTreeAction('local-ocr', detail); + recordFileTreeAction('knowledge-rag-index', detail); recordFileTreeActionStatus('pending', detail); void runLocalOcrForDetail(detail, trigger).then(function(job) { - recordFileTreeActionStatus(String(job && job.status || 'done'), detail); + recordFileTreeActionStatus(job && job.retryRequired ? 'retry' : 'done', detail); }).catch(function(error) { recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' })); - window.alert(error && error.message ? error.message : 'OCR 生成失败'); + window.alert(error && error.message ? error.message : '资料库索引失败'); + }); + return; + } + if (action === 'knowledge-rag-index') { + recordFileTreeAction('knowledge-rag-index', detail); + recordFileTreeActionStatus('pending', detail); + void ingestKnowledgeRagForDetail(detail, trigger).then(function() { + recordFileTreeActionStatus('done', detail); + }).catch(function(error) { + recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' })); + window.alert(error && error.message ? error.message : '资料库索引失败'); }); return; } @@ -1010,10 +1017,10 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => { { separator: true }, { action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, destructive: true, requiresApproval: true, when: '!workspace.readonly && !editor.dirty' } ]; - if (localOcrSupported) { - var ocrItem = { action: 'local-ocr', icon: 'auto_awesome', label: '生成 OCR', when: '!workspace.readonly' }; - var insertAt = isAttachment ? 11 : isAsset ? 3 : -1; - if (insertAt >= 0) items.splice(insertAt, 0, ocrItem); + if (currentSourceKind() === 'local_folder' && detail.rowKind !== 'index') { + var ragItem = { action: 'knowledge-rag-index', icon: 'travel_explore', label: '加入资料库索引', when: '!workspace.readonly' }; + var insertAt = isAttachment ? 11 : isAsset ? 3 : 3; + if (insertAt >= 0) items.splice(insertAt, 0, ragItem); } items.forEach(function(item) { if (item.when !== undefined && !evaluateSidebarFileTreeWhen(ctx, item.when)) { diff --git a/rust/crates/mnote-web/browser/sidebar-page-ai-target-runtime.js b/rust/crates/mnote-web/browser/sidebar-page-ai-target-runtime.js index 69a5920e..5f4ed9c8 100644 --- a/rust/crates/mnote-web/browser/sidebar-page-ai-target-runtime.js +++ b/rust/crates/mnote-web/browser/sidebar-page-ai-target-runtime.js @@ -443,43 +443,7 @@ export function createSidebarPageAiTargetRuntime(context) { } async function fetchPageAiOcrSidecarContext(editorTarget) { - if (currentSourceKind() !== 'local_folder') return null; - var target = editorTarget && typeof editorTarget === 'object' ? editorTarget : {}; - var workspacePath = target.workspacePath && typeof target.workspacePath === 'object' ? target.workspacePath : {}; - var relativePath = String(workspacePath.relativePath || target.path || '').trim(); - var resourceKind = String(target.resourceKind || target.editorKind || workspacePath.resourceKind || '').trim(); - if (!relativePath || !pageAiOcrEligiblePath(relativePath) || !pageAiOcrEligibleResourceKind(resourceKind)) return null; - var rootUri = String(workspacePath.rootUri || currentRootUri() || '').trim(); - if (!rootUri) return null; - try { - var statusUrl = new URL('/api/local-folder/ocr/status', window.location.origin); - statusUrl.searchParams.set('rootUri', rootUri); - statusUrl.searchParams.set('sourceRootRelativePath', relativePath); - var statusResponse = await fetch(statusUrl.toString(), { cache: 'no-store', headers: { accept: 'application/json' } }); - var statusPayload = await statusResponse.json().catch(function() { return null; }); - var job = statusResponse.ok && statusPayload && statusPayload.ok === true ? statusPayload.job : null; - var ocrPath = String(job && job.ocrRootRelativePath || '').trim(); - if (!ocrPath || String(job.status || '') === 'failed') return null; - var readUrl = new URL('/api/local-folder/ocr/read', window.location.origin); - readUrl.searchParams.set('rootUri', rootUri); - readUrl.searchParams.set('ocrRootRelativePath', ocrPath); - var readResponse = await fetch(readUrl.toString(), { cache: 'no-store', headers: { accept: 'application/json' } }); - var readPayload = await readResponse.json().catch(function() { return null; }); - if (!readResponse.ok || !readPayload || readPayload.ok !== true) return null; - return { - schema: 'mnote.local_ocr_context.v1', - source: 'local_ocr_sidecar', - sourceRootRelativePath: relativePath, - ocrRootRelativePath: ocrPath, - status: String(job.status || ''), - stale: job.stale === true, - provider: String(job.provider || ''), - modelVersion: String(job.modelVersion || ''), - plainTextPreview: pageAiOcrBodyPreview(readPayload.markdown || ''), - }; - } catch (_) { - return null; - } + return null; } async function pageAiEnrichOcrContextRefs(contextRefs, agentTargetPackage, editorTarget) { diff --git a/rust/crates/mnote-web/browser/sidebar-page-settings-runtime.js b/rust/crates/mnote-web/browser/sidebar-page-settings-runtime.js index 3909d14b..4b1e223c 100644 --- a/rust/crates/mnote-web/browser/sidebar-page-settings-runtime.js +++ b/rust/crates/mnote-web/browser/sidebar-page-settings-runtime.js @@ -71,6 +71,15 @@ export function createSidebarPageSettingsRuntime(context) { return Object.assign({ 'localOcr.autoEnabled': false }, pageUiState.localOcrPreferences || {}); } + function currentKnowledgeRagSummary() { + return pageUiState.knowledgeRagSummary || {}; + } + + var knowledgeRagBridgeTimer = 0; + var knowledgeRagBridgeGeneration = 0; + var knowledgeRagBridgeAttempt = 0; + var knowledgeRagBridgeDelays = [1200, 2000, 3200, 5000, 8000, 13000, 21000, 34000]; + function pageWidthModeLabel(mode) { if (mode === 'inherit') return '继承默认'; if (mode === 'readable') return '阅读'; @@ -337,6 +346,12 @@ export function createSidebarPageSettingsRuntime(context) { ocrTrigger.setAttribute('data-state', ocrOpen ? 'open' : 'closed'); ocrTrigger.setAttribute('aria-expanded', ocrOpen ? 'true' : 'false'); } + var ragTrigger = document.querySelector('[data-testid="mnote-knowledge-rag-settings-toggle"]'); + var ragOpen = isKnowledgeRagSettingsOpen(); + if (ragTrigger instanceof HTMLElement) { + ragTrigger.setAttribute('data-state', ragOpen ? 'open' : 'closed'); + ragTrigger.setAttribute('aria-expanded', ragOpen ? 'true' : 'false'); + } } function createPageOptionRow(key, type) { @@ -380,8 +395,8 @@ export function createSidebarPageSettingsRuntime(context) { return '' + ''; @@ -659,27 +674,53 @@ export function createSidebarPageSettingsRuntime(context) { } function ensureLocalOcrSettingsPopover() { - var existing = document.querySelector('[data-testid="mnote-local-ocr-settings-popover"]'); - if (existing instanceof HTMLElement) return existing; - var popover = document.createElement('div'); - popover.className = 'wolai-page-settings-popover mnote-local-ocr-settings-popover'; - popover.setAttribute('data-testid', 'mnote-local-ocr-settings-popover'); - popover.setAttribute('data-mnote-surface', 'local-ocr-settings'); - popover.hidden = true; - popover.innerHTML = '' + - '