Files
mnote/design/05-editor-mainline/process/5-38-bidirectional-link-system-v1.md
T

13 KiB
Raw Blame History

5-38 双向链接系统(Bidirectional Linksv1

状态:process(调研完成,待分阶段落地)
日期:2026-07-29
目标:避免「多处引用同一知识却各写一份」的多真源问题;在 local-first + tree-first kernel 下建立单真源引用边 + 反链投影


0. 一句话结论

正向链接写在正文(Markdown / block 语义)→ 保存/索引时抽出 references 边 → 反链只是该边的反向投影。
TipTap 只负责插入/渲染;在前端维护第二套链接图。


1. TipTap 更新检查(2026-07-29

位置 版本 说明
reference-code/leptos-tiptap/tiptap @tiptap/* 2.27.2 当前主链 bridge 依赖
npm v2-latest 2.27.2 v2 线已停在此点
npm latest 3.29.22026-07-28 主线已进 v3 很久
reference-code/tiptap-main 快照 packages 约 3.23.4git ~2026-05 参考仓库偏旧于 npm latest
@tiptap/extension-mention 3.29.2 与 core 对齐 v3 线 mentionv2 仍有独立包

1.1 对双向链接的含义

  • 双向链接不依赖 TipTap 3。核心是 kernel 边 + Markdown 持久化 + 索引投影。
  • 本阶段不建议为双向链接单独升 v3。v2→v3 是独立迁移(schema/API/extension 打包方式变化),成本远高于「补 page mention + 反链面板」。
  • 可继续参考:
    • reference-code/leptos-tiptap(当前 bridge / link extension
    • reference-code/tiptap-notion-like-registrymention-dropdown-menu / link-popoverUI 交互,不是用户 mention 语义)
    • reference-code/tiptap-main 仅作 API 对照;升 v3 时再同步拉取

1.2 可选后续(非本系统阻塞)

  • 独立 checklist:评估 leptos-tiptap bridge 升 @tiptap/*@3 的 breaking 面。
  • 升版前冻结:现有 page_reference ↔ link mark round-trip 测试。

2. 现状盘点(已有资产 / 缺口)

2.1 已有(可复用,勿重造)

资产 路径/入口
语法 [[page|label]] / ((block|label)) mnote-editor-core/src/markdown.rs
块类型 page_reference / PageReference core-protocol、bridge、editor actor
编辑投影 page_ref → 段落 + link markclass mnote-page-block-link document-tiptap-conversion-runtime.jsblock_transform.rs
索引反链 ](*.md) + [[…]]API 可读 local_search_index.rsGET /api/search/local-index/backlinks
Kernel 类型 KernelEdgeType::References / BacklinksTo core-protocol/src/kernel.rs
页面选项 collapseBacklinks / showBlockRefCount page aggregate + UI preferences
证据边 markdown 链接 → deterministic edges document evidence / LightRAG 旁路(勿与笔记反链混为同一产品入口)
思源边界 借产品闭环,不借 .sy / SQL 真相 design/09-siyuan-reference/...

2.2 缺口(多真源风险点)

  1. 编辑器层page_ref 降级为普通 link mark,无独立 inline atom[[ 输入规则 / 页面搜索插入不完整(legacy editor 仅插 [[page]] 占位)。
  2. 索引层extract_backlinks 存的是字符串目标(路径 / wikilink 文本),未统一解析为稳定 documentId / ObjectIdentity;重命名后易漂。
  3. Kernel 层References / BacklinksTo 已声明,成为 page_aggregate / 反链 UI 的正式 query 面。
  4. UI 层collapseBacklinks 明确 「待接线:当前 Rust 壳未挂回链面板」;旧 local-index 反链 DOM 已故意不复活(避免与 LightRAG 资料库入口混淆)。
  5. 语义层E28 @mention 已暂停且纠偏为「人/会议」,不是页面引用;页面引用应走 [[ / slash「页面」/ 转 page_reference,勿与 @ 混用。
  6. 块引用((block)) 语法与类型有,主编辑岛与稳定 blockId 持久化尚未闭环。

3. 产品语义(对齐思源 / Wolai,落 MNote 边界)

3.1 三种引用(必须分开)

种类 用户形态 持久化 索引边 优先级
页面引用 [[Title]] / [[path|显示名]] / page_reference 块 Markdown 或 block props references → page P0
块引用 ((blockId)) 需稳定 blockId references → block P1
外链 / 附件 [x](url) / 资源 path 普通 link / resource_ref 不进「笔记反链」主列表(可进 resourceRefs 已有,保持

3.2 「双向」的真正含义

  • 用户只写正向引用。
  • 系统维护 source → target 边。
  • 反链 = target ← sources 的投影查询,不是第二份手写列表。
  • 图谱 / 搜索 / AI 上下文 / 导图挂件 只读同一套边,禁止各自扫一遍 Markdown 各存一份。

3.3 真源顺序(硬规则)

本地 .md(+ 将来稳定 block 元数据)
        ↓ 解析 / 保存
Page Aggregate blocks(含 page_reference / 将来 inline ref
        ↓ 抽出
Reference edgeskernel 或 local-index 统一表,单一实现)
        ↓ 投影
反链面板 / search / AI tools / 可选图谱

禁止

  • 浏览器 localStorage 存链接图
  • TipTap JSON 与 Markdown 各维护一套「官方」反链
  • LightRAG entity 边冒充笔记 page backlink(可并列展示,须分入口)

4. 目标架构

4.1 引用边最小模型

ReferenceEdge {
  source_document_id: String,   // local-md:… 或稳定 id
  source_block_id:    Option<String>,
  target_kind:        page | block | resource | external,
  target_document_id: Option<String>,  // 解析成功时
  target_raw:         String,          // 原文 token / path,解析失败仍可展示
  anchor_text:        Option<String>,
  locator:            Option<String>,  // 行号 / offset,便于跳转
}
  • 写入:文档保存成功、local-index refresh、watcher 变更后 整页替换该 source 的出边(思源式:按 root 删再插,避免脏边)。
  • 读取
    • query.backlinks_to(documentId) → 谁链到我
    • query.references_from(documentId) → 我链向谁
  • 与已有 API 对齐:先让 GET /api/search/local-index/backlinks 成为上述 query 的 HTTP 面,再视需要升为 kernel query 名 search.local_index.backlinks → 最终 kernel.backlinks_to

4.2 目标解析(消灭字符串多真源)

解析顺序建议:

  1. 已是 local-md:… / 已知 documentId
  2. 相对路径相对当前文件 resolve → 规范化相对 root → local-md:{encode}
  3. wikilink [[Title]]:按标题 / 文件名 stem 在索引内消歧(多命中则 raw 保留 + UI 标黄)
  4. 失败:target_document_id = nulltarget_raw 保留,反链仍可按 raw 弱匹配(可选)

重命名 / 移动:走现有 tree command;索引 rebuild 时重算边(P0 可全量 rebuildP1 增量 + transferBlockRef 类迁移可参考思源 transferBlockRef照搬 SQL)。

4.3 TipTap 显示层(v2 即可)

不要把 Notion registry 的「用户 Mention」直接当页面引用。

推荐两档:

形态 说明
A(最小,先做) 保持 paragraph + link mark + mnote-page-block-link,补强 round-trip 与点击打开 已有主链,改动小
B(产品完整) 新增 inline atompageMention / 或专用 mark attrsdata-mnote-ref="page" + data-target-id 便于样式、未解析态、禁止拆字

输入:

  • InputRule[[ 打开页面搜索(复用 local search / tree query不是 @
  • Slash:插入「页面引用」
  • 块菜单:段落 ↔ page_reference 转换(已有 replace_block_with_page_reference 可扩)

Markdown 导出:

  • 优先 [[target|label]][label](rel/path.md)工作区内统一一种;建议 wikilink 作规范、md link 作兼容导入
  • mnote-editor-core 已用 [[id|label]]local-folder 路径型文档应导出 相对路径 以便 Git 可读:[[docs/child|子页]][子页](docs/child.md)实现前定一种 SSOT 写进本文件 §6

4.4 反链 UI

  • 挂在页面底部或右侧信息区Wolai/思源心智),不要塞回已移除的 local-index 设置页。
  • 消费:backlinks API + Page Aggregate 选项 collapseBacklinks
  • 每条:来源标题、路径、可选 snippet、点击 open document(带 sourceKind/rootUri)。
  • showBlockRefCount:有块引用后再接线。

4.5 与 AI / LightRAG

需求 入口
「哪些笔记链到这篇」 笔记反链 API / kernel edges
「资料库里实体/PDF 证据」 LightRAG / knowledge-rag
Agent 写引用 必须写 Markdown/block 合法引用,再走同一抽取器

5. 分阶段落地(可执行)

Phase 0 — 契约冻结(0.51d

  • 选定工作区持久化规范:[[rel/path|label]] [label](rel.md) 为写出 SSOT(导入两者都认)。
  • 固定 ReferenceEdge 字段与 documentId 解析顺序(§4.2)。
  • 明确 @ ≠ 页面引用(文档 + slash 文案)。

Phase 1 — 索引单真源(1–2d)【优先】

  • 强化 extract_backlinks:输出结构化 edge(含 resolved id),不只是字符串列表。
  • query_local_backlinksresolved documentId 聚合;兼容旧字符串。
  • 单测:A→B 的 [[B]][B](b.md) 反链一致;重命名后 refresh 更新。
  • 保持 task452 语义:API 可用;不恢复旧 settings DOM。

Phase 2 — 反链面板(12d

  • 文档页挂载 backlinks panel(读 Phase 1 API)。
  • 接通 collapseBacklinks
  • Smoke:两篇本地 md 互链,B 页可见 A。

Phase 3 — 编辑器插入体验(2–3d)

  • [[ 触发页面搜索 + 插入规范 token / page_reference。
  • 保存后索引增量/全量更新边。
  • 未解析目标的视觉态(可选)。
  • 仍用 TipTap 2.27.2;不夹带 v3。

Phase 4 — Kernel 边对齐(按主线空档)

  • local-index 实现升为 KernelEdgeType::References 的唯一写入路径(或薄封装)。
  • BacklinksTo 作为派生查询,不双写。
  • Page Aggregate 可附带 backlinks 摘要(注意体积与权限)。

Phase 5 — 块引用(后置)

  • 稳定 blockId(与 UniqueID / 导出策略一起设计)。
  • ((id)) 解析、预览、transfer on move。
  • showBlockRefCount

明确不做(本 v1

  • 为双向链接升级 TipTap 3
  • 复制思源 refs SQL 表为产品真相
  • 用 LightRAG 替代笔记反链
  • 恢复旧 local-index 设置页反链 DOM
  • @ 改成页面引用

6. 待决事项(实现前二选一写死)

  1. 写出格式 SSOT

    • 方案 Wwikilink [[rel/path|label]](贴近思源/Obsidianeditor-core 已有)
    • 方案 M:标准 md link [label](rel.md)Git 友好、通用)
    • 建议:索引与解析两者都认;新插入默认 W,导出可配置(默认 W)。
  2. page_reference 块 vs inline

    • 块级:独占一段(当前)
    • 行内:段落中的 mention atom
    • 建议P0 块级 + link markP1 行内 atom(真正「文中引用」)。
  3. 反链面板位置

    • 页底固定区 vs 右侧 drawer
    • 建议:页底折叠区(collapseBacklinks 已存在语义)。

7. 参考索引

来源 用途
思源 kernel/sql refs 表 / block_ref_query.go 边模型、按 root 重建、反链查询形状
思源 conf Backlink* / VirtualBlockRef 产品选项;虚拟引用可后置
design/09-siyuan-reference/... 借鉴边界
design/01-tree-first-graph-kernel/... tree + references 边
leptos-tiptap tiptap_link.ts link mark bridge
notion-like mention-dropdown-menu 仅 suggestion UI 模式
local_search_index.rs extract_backlinks 现抽取实现
sidebar-page-settings-runtime.js collapseBacklinks 待接线文案

8. 验收标准(Phase 1–2 最小闭环)

  1. 在 root 下创建 A.md,正文写入指向 B.md 的规范引用并保存。
  2. 打开 B:反链面板出现 A(标题可点,带 local source 参数)。
  3. 删除 A 中引用并保存 / 刷新索引后,B 反链消失。
  4. 同一套数据:GET .../local-index/backlinks 与面板一致。
  5. 无第二套前端链接缓存;无 TipTap 3 升级。

9. 与主线关系

当前主线仍是 Page Aggregate / tree / local-first。双向链接应:

  • 吃透已有 local-index + page_referencePhase 12
  • 编辑体验(Phase 3
  • Kernel 图边与块引用跟 tree-first 空档合并,避免另起存储

本文是 process 设计;落地时按 Phase 开 checklist / smoke,完成后迁 done/