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

281 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.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 线 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-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` markclass `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 缺口(多真源风险点)
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 引用边最小模型
```text
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 = null``target_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 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 设置页。
- 消费:`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 — 索引单真源(12d**【优先】**
- [ ] 强化 `extract_backlinks`:输出结构化 edge(含 resolved id),不只是字符串列表。
- [ ] `query_local_backlinks`**resolved 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 12 最小闭环)
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/`。*