13 KiB
13 KiB
5-38 双向链接系统(Bidirectional Links)v1
状态: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.2(2026-07-28) | 主线已进 v3 很久 |
reference-code/tiptap-main 快照 |
packages 约 3.23.4,git ~2026-05 | 参考仓库偏旧于 npm latest |
@tiptap/extension-mention |
3.29.2 与 core 对齐 | v3 线 mention;v2 仍有独立包 |
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-registry的mention-dropdown-menu/link-popover(UI 交互,不是用户 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 mark(class mnote-page-block-link) |
document-tiptap-conversion-runtime.js、block_transform.rs |
| 索引反链 | 扫 ](*.md) + [[…]],API 可读 |
local_search_index.rs → GET /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 缺口(多真源风险点)
- 编辑器层:page_ref 降级为普通 link mark,无独立 inline atom;
[[输入规则 / 页面搜索插入不完整(legacy editor 仅插[[page]]占位)。 - 索引层:
extract_backlinks存的是字符串目标(路径 / wikilink 文本),未统一解析为稳定documentId/ ObjectIdentity;重命名后易漂。 - Kernel 层:
References/BacklinksTo已声明,未成为 page_aggregate / 反链 UI 的正式 query 面。 - UI 层:
collapseBacklinks明确 「待接线:当前 Rust 壳未挂回链面板」;旧 local-index 反链 DOM 已故意不复活(避免与 LightRAG 资料库入口混淆)。 - 语义层:E28
@mention已暂停且纠偏为「人/会议」,不是页面引用;页面引用应走[[/ slash「页面」/ 转 page_reference,勿与@混用。 - 块引用:
((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 edges(kernel 或 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 目标解析(消灭字符串多真源)
解析顺序建议:
- 已是
local-md:…/ 已知 documentId - 相对路径相对当前文件 resolve → 规范化相对 root →
local-md:{encode} - wikilink
[[Title]]:按标题 / 文件名 stem 在索引内消歧(多命中则 raw 保留 + UI 标黄) - 失败:
target_document_id = null,target_raw保留,反链仍可按 raw 弱匹配(可选)
重命名 / 移动:走现有 tree command;索引 rebuild 时重算边(P0 可全量 rebuild;P1 增量 + transferBlockRef 类迁移可参考思源 transferBlockRef,不照搬 SQL)。
4.3 TipTap 显示层(v2 即可)
不要把 Notion registry 的「用户 Mention」直接当页面引用。
推荐两档:
| 档 | 形态 | 说明 |
|---|---|---|
| A(最小,先做) | 保持 paragraph + link mark + mnote-page-block-link,补强 round-trip 与点击打开 |
已有主链,改动小 |
| B(产品完整) | 新增 inline atom:pageMention / 或专用 mark attrs:data-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 设置页。
- 消费:
backlinksAPI + 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.5–1d)
- 选定工作区持久化规范:
[[rel/path|label]]或[label](rel.md)为写出 SSOT(导入两者都认)。 - 固定
ReferenceEdge字段与documentId解析顺序(§4.2)。 - 明确
@≠ 页面引用(文档 + slash 文案)。
Phase 1 — 索引单真源(1–2d)【优先】
- 强化
extract_backlinks:输出结构化 edge(含 resolved id),不只是字符串列表。 query_local_backlinks按 resolved documentId 聚合;兼容旧字符串。- 单测:A→B 的
[[B]]与[B](b.md)反链一致;重命名后 refresh 更新。 - 保持
task452语义:API 可用;不恢复旧 settings DOM。
Phase 2 — 反链面板(1–2d)
- 文档页挂载 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
- 复制思源
refsSQL 表为产品真相 - 用 LightRAG 替代笔记反链
- 恢复旧 local-index 设置页反链 DOM
- 把
@改成页面引用
6. 待决事项(实现前二选一写死)
-
写出格式 SSOT
- 方案 W:wikilink
[[rel/path|label]](贴近思源/Obsidian,editor-core 已有) - 方案 M:标准 md link
[label](rel.md)(Git 友好、通用) - 建议:索引与解析两者都认;新插入默认 W,导出可配置(默认 W)。
- 方案 W:wikilink
-
page_reference 块 vs inline
- 块级:独占一段(当前)
- 行内:段落中的 mention atom
- 建议:P0 块级 + link mark;P1 行内 atom(真正「文中引用」)。
-
反链面板位置
- 页底固定区 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 最小闭环)
- 在 root 下创建
A.md,正文写入指向B.md的规范引用并保存。 - 打开
B:反链面板出现A(标题可点,带 local source 参数)。 - 删除 A 中引用并保存 / 刷新索引后,B 反链消失。
- 同一套数据:
GET .../local-index/backlinks与面板一致。 - 无第二套前端链接缓存;无 TipTap 3 升级。
9. 与主线关系
当前主线仍是 Page Aggregate / tree / local-first。双向链接应:
- 先吃透已有 local-index + page_reference(Phase 1–2)
- 再编辑体验(Phase 3)
- Kernel 图边与块引用跟 tree-first 空档合并,避免另起存储
本文是 process 设计;落地时按 Phase 开 checklist / smoke,完成后迁 done/。