现代编辑器与协作技术栈:从一次输入到可靠保存
AI 参与说明(Agent:Devin / Fusion):本文依据公开文档与源码核对能力;架构示意是综合解释,不构成某个组合的运行或性能认证。资料核查日为 2026-10-28,执行入口为 Linear。本轮 Context7 请求因配额耗尽未取得结果,改查文末所列官方来源。Fusion 为会话模式,具体模型标识与 reasoning effort 未取得任务级运行记录。
两个人同时改一段文字,互相看见光标,刷新后内容还在。这看起来是一个“编辑器功能”,实际上包含三个不同的承诺:输入能正确进入文档,并发修改能按规则合并,已经确认保存的内容能在故障后恢复。
把 Tiptap、Yjs 和一个 WebSocket 服务装在一起,还不能兑现这些承诺。把“对端收到了”当作“数据库存好了”,或把一份 JSON 当作完整的协作历史,都可能在断线、重启时丢失修改。我们从一次编辑开始看。
先认识几个贯穿全文的名称
| 英文术语 | 中文名称 | 简要解释 |
|---|---|---|
| Schema | 模式约束 | 定义哪些节点、属性和嵌套结构合法 |
| Transaction | 事务 | 一次编辑状态变更,可能包含多个文档修改及选区变化 |
| CRDT | 无冲突复制数据类型 | 按约定规则合并并发修改,使收到相同必要信息的副本收敛 |
| Binding | 绑定层 | 把编辑器文档与协作数据模型中的修改相互映射 |
| Provider | 同步提供器 | 交换同步消息、维护连接与重连的接线模块 |
| Awareness | 感知状态 | 在线用户、光标等短暂状态,不是持久正文 |
| State Vector | 状态向量 | 描述 Yjs 副本已知的客户端逻辑时钟前缀 |
| Snapshot | 快照 | 引擎定义的状态或恢复表示,不天然等于产品版本 |
| Projection | 派生投影 | 从权威数据生成的 HTML、搜索索引等表示 |
一次输入不是一条 WebSocket 消息
假设两人打开同一份会议记录。你在段落中输入“周五”,另一人把这个段落改为列表。浏览器先处理输入法组合输入(IME composition)、选区与 DOM;编辑器再把这些变化解释成自身模型中的修改。工具栏和快捷键也是修改入口,并不比键盘更接近数据库。
contentEditable 提供可编辑区域,却不知道产品中的“任务项”“提及”或“评论锚点”是什么。现代编辑器因此通常维护独立模型,而不是直接拿 DOM 当正文。DOM 还夹着展示样式、浏览器修复和临时交互元素;模型中的位置,也不能直接用 DOM 节点下标代替。
flowchart TB
B["浏览器输入 / IME / Selection"] --> E["编辑器内核:文档与修改"]
F["框架 / UI:命令、工具栏、块菜单"] --> E
E --> V["渲染视图"]
E <--> A["Binding:模型与位置映射"]
A <--> C["CRDT 文档"]
C <--> P["Provider:同步与连接"]
P <--> R["房间服务:授权与生命周期"]
C <--> L["本地持久化"]
R <--> D["服务端持久化"]
D --> Q["Projection:搜索 / HTML"]
图中是职责关系,不是必须依次经过的函数调用。Binding 位于编辑器与 CRDT 之间;远端修改沿反方向进入编辑器。Provider 不应把远端应用产生的事件再次当成新的本地编辑无限回传。实际库可能跨越几层,但每个边界仍要有人负责。
三种内核,三种修改语言
编辑器内核决定“正文是什么”和“怎样修改它”。框架可以包装这些规则,却不能让它们消失。
| 内核 | 文档与状态 | 修改机制 | 扩展时绕不开的约束 |
|---|---|---|---|
| ProseMirror | Schema 约束的节点树;EditorState 包含正文、选区及插件状态 | Transaction 可包含多个 Step,并携带映射与元数据 | 节点结构、位置映射、插件状态和命令 |
| Slate | 由 Element、Text 等组成的树;Editor 管理选区和操作 | 高层 Transforms 产生底层 Operations;normalization 修复结构 | 路径、操作语义、内建及自定义 normalization |
| Lexical | EditorState 包含节点树和选区;提交后的状态不可变 | 在 editor.update() 上下文中修改待提交状态,再协调渲染 | 节点注册、读写上下文、命令与节点转换 |
ProseMirror:不要把 Transaction 等同于正文差异
ProseMirror applies transactions to produce a new editor state. 按 ProseMirror Guide 的定义,Transaction 可以插入文本、设置选区、携带插件元数据,也可能根本不改正文。Step 是文档变换单元,不是整个编辑器状态。
例如在段落前插入标题后,旧位置需要经映射才能继续使用。评论若只记住“第 18 个位置”,后续插入就可能把它指向别处。协作 Binding 还要处理不同副本暂时看到不同文档时的位置关系。
Schema 不是 TypeScript 类型检查的另一种写法。它描述运行时文档允许哪些节点、属性及子节点组合。双方安装不同节点扩展,即使更新字节成功送达,也可能对同一结构有不同解释。
Slate:灵活不等于没有约束
Slate 的 Operations 表达插入文本、移除节点、移动节点等基本修改;常用的 Transforms 是生成这些操作的高层接口。Normalization 则把内容修复到合法结构。
这让产品能定义自己的文档形态,但也带来协作要求:修复规则本身可能产生操作。不同客户端若使用互相矛盾的规则,一个拆节点、另一个又合并,CRDT 不会替应用决定哪套 Schema 才是正确的。复合变换还要考虑中间态是否需要暂缓 normalization,不能只看最终 JSON 漂不漂亮。
Lexical:EditorState 也不是 DOM 截图
Lexical 的 EditorState 是独立于 DOM 的状态模型。更新期间可以修改待提交状态,提交后作为不可变状态读取。$getRoot() 等 API 依赖当前读写上下文,并不是任何异步回调中都能调用的普通工具函数。
还要区分内存对象和序列化结果:当前文档说明,toJSON() 序列化文档树,不包含选区和运行时节点 key。因此“保存了 EditorState JSON”不代表恢复了用户全部会话状态,更不代表保存了协作协议的因果信息。
框架替你组织编辑行为,不替你完成产品
| 上层项目 | 建立在哪些基础上 | 主要减轻的工作 | 宿主仍要负责 |
|---|---|---|---|
| Tiptap | ProseMirror | Extension、节点与标记、命令、框架接入 | 产品 UI、权限、资源、版本与保存语义 |
| Plate | Slate | 插件组织、常见编辑能力与配套 UI | 文档规则、业务状态、协作服务与数据生命周期 |
| BlockNote | Tiptap / ProseMirror 等基础能力 | 面向块的模型、插块和拖块等编辑体验 | 自定义块语义、权限、持久化、导入导出边界 |
Tiptap 不是独立于 ProseMirror 的另一套底层内核;Plate 也不是与 Slate 无关的 UI 包。Tiptap Introduction、Plate 文档 和 BlockNote 内部节点说明 能帮助定位这些关系。
BlockNote 尤其容易让人混淆三种表示:面向应用的块 JSON、内部 ProseMirror 文档、协作使用的 Yjs 数据。它们描述有关联的内容,但结构和用途不同,不能直接互相当作恢复文件。BlockNote Document Structure 适合理解应用层看到的块;内部表示则要结合实现阅读。
选型时,与其问“谁支持表格”,不如追问一个具体动作:把带评论的表格行从一个块移动到另一个块,选区、撤销、远端光标、导出和旧客户端会怎样?成熟框架能减少接线工作,但这个产品行为仍需宿主定义并验收。
序列化:四种“保存”保存的不是同一个东西
| 表示 | 适合什么 | 通常不能单独保留什么 |
|---|---|---|
| 编辑器 JSON | 在匹配的 Schema 下重新编辑、服务端处理结构 | 完整协作身份、因果历史、临时选区 |
| HTML | 展示、跨系统交换与部分剪贴板场景 | 所有自定义节点语义、稳定 ID、协作历史 |
| Markdown | 文本发布、版本管理、受限语义交换 | 任意块属性、复杂表格、评论及完整富文本语义 |
| CRDT update / 引擎恢复编码 | 同步副本、恢复协作状态 | 产品 ACL、附件本体、业务审计和发布版本的全部规则 |
“都能转成 JSON”不是格式兼容保证。导出器可能丢掉自定义块的属性;重新导入可见正文,也可能为内容生成新的协作身份。反过来,二进制 CRDT 数据也不能脱离正确的 Binding、Schema 和版本约定,自动变成可显示的文章。
一个实用划分是:选定权威恢复数据,再生成 HTML、Markdown 和搜索索引作为 Projection。若业务同时允许直接写正文 JSON 和 CRDT,就必须定义二者如何协调;不能让最后写入的那份整篇 JSON 悄悄覆盖协作内容。
并发:中心排序与 CRDT 不是“有无服务器”之争
仍看会议记录的例子。两人从同一个版本出发,各自插入文字。若双方只发送“把第 10 个位置替换成某字符串”,收包时的第 10 个位置可能早已改变。
ProseMirror 内置的 collab 模型 采用中心权威确定 Step 顺序。客户端保留尚未确认的本地修改,接收权威顺序中的修改后,对自己的未确认修改做 rebase。这里的重点是共同顺序与位置映射,不是简单按服务器时间覆盖全文。OT 类方案也围绕并发操作变换展开,但不能把所有中心同步协议都当作同一套 OT 实现。
CRDT 为内容或操作建立稳定身份及合并规则,使副本能在满足协议前提、最终收到必要信息后收敛。它可以通过中心服务器转发、持久化和授权,并不要求浏览器直接 P2P 连接。反过来,WebRTC 只是传输方式,不会自动解决并发语义。
收敛还不等于符合业务意图。两人同时把一个标题改成两个不同标题,合并结果即使在所有副本上一致,也未必是任何一人想要的最终文案。产品仍可能需要冲突提示、审阅或人工选择。
Yjs:update 解决交换,Binding 解决语义
Yjs 的 Y.Doc 可以包含 Y.Text、Y.Map、Y.Array、XML 类型等共享数据。选哪个类型,取决于编辑器 Binding 和应用模型,不是所有编辑器都监听一个名叫 content 的字段。
update 与 State Vector
Yjs document updates are commutative, associative and idempotent. Document Updates 用这些性质说明:同一批必要更新可以乱序、重复应用,最终仍能收敛;这不允许永久丢弃更新。
State Vector 记录各客户端已知逻辑时钟前缀,对端据此计算差量。它不是在线人数、业务版本号、正文哈希或数据库收据。尤其不能只因为两个 State Vector 相同就宣布“正文一致且已保存”:删除信息还要通过更新传播,落盘确认又是另一层协议。
可以用一个不涉及网络的思想实验理解它:两个副本先共享 AB,断开后分别在中间插入 X 与 Y;随后交换各自缺失的 update,再重复投递其中一份。目标是两边内容一致,且重复投递不多插一个字符。并发字符的先后由 CRDT 规则决定,测试不能擅自断言一定是 AXYB。这个实验只说明合并性质,不证明 Provider、权限或保存链路已经可用。
三个模块,三种故障
| 模块 | 负责的事情 | 典型接线 | 单独解决不了的问题 |
|---|---|---|---|
| Binding | 模型变更、位置和协作编辑语义之间的映射 | y-prosemirror | 身份认证、服务端存储 |
| Provider | 同步握手、传输、重连及可选 Awareness | y-websocket 等 | 业务定义的持久化确认 |
| 持久化 adapter | 将更新或检查点写入介质并加载 | y-indexeddb、服务端数据库接线 | 编辑器格式转换、业务 ACL |
这里以已核对的 y-prosemirror v1.3.7 为具体例子:它把 Y.XmlFragment 映射到 ProseMirror state,并提供同步、光标和协作撤销插件。这是明确版本的说明,不把主分支未来的内部表示当作所有发行版的保证。更多 Binding 见 Yjs Editor Bindings。
将完整文档 JSON 反复放进 Y.Map 的某个键,并不会让这个 JSON 的每个内部字段自动变成独立共享类型。这样得到的往往是整值替换,而不是富文本 Binding 对字符、节点、选区和撤销的适配。两个用户改不同段落,也可能在同一个顶层值上竞争。
Awareness 和 UndoManager 分别守住什么边界
Awareness 传播短暂的在线状态和光标,不应作为正文存进持久 CRDT。连接消失后,presence 应随断开或超时失效。它携带的名字、头像或 user.id 是展示负载,不能充当服务端认证凭据。长期评论需要持久锚点、删除后的行为及迁移策略,不能拿临时光标或 DOM 下标凑用。
UndoManager 可以按共享类型范围、transaction origin 和捕获分组追踪修改。协作撤销需要撤掉所选范围内的本地操作,而不是用“昨天的整篇 JSON”覆盖现在的文档。origin 是本地接线中的来源标识,不是通过网络验证过的用户身份。
Tiptap Collaboration 因此要求避免默认历史机制与协作历史重复接管撤销。当前文档使用 StarterKit 的 undoRedo: false;旧版本配置名可能不同,接入时按实际依赖核对。协作撤销也不等于永久审计日志,页面关闭后要不要保留撤销历史,是另外的产品决定。
Loro:把已知历史和当前可见状态拆开
理解 Loro 最有用的入口不是“另一种 Yjs”,而是 OpLog 与 DocState 的分离:OpLog 记录操作与因果历史;DocState 是某个版本下物化出的可见内容。
同一个文档,可以知道新历史却显示旧状态
当文档 checkout 到旧版本,DocState 可以停留在旧状态,OpLog 仍保有后来收到的历史。因此 doc.version() 和 doc.oplogVersion() 不一定相同,对应的 frontiers() 与 oplogFrontiers() 也必须区分。默认的 detached 状态下不能随意编辑;不要把历史浏览直接等同于创建可写分支。
这也是同步代码不能只看屏幕上正文的原因:界面正在看旧版本,不代表副本没有收到新操作。中继若不需要显示正文,也有机会只处理 OpLog,避免不必要的状态物化;具体实现仍需按 API 与运行环境验证。
Version Vector 与 Frontiers
Version Vector 显式描述各 peer 的操作前缀;Frontiers 用因果图的边界操作集合表示一个版本,以及这些操作的因果祖先。这里的 peer 是副本身份,不是已认证的用户账户。
Frontiers 常比逐 peer 列举紧凑,但要理解它包含哪些历史,通常需要相应的因果信息。它不是“每个用户最后一次修改的时间戳”,也不保证只有一个元素。两个并发分支尚未被后续操作共同包含时,边界可以有多个操作。两种版本表示不是因为都叫“版本”就能互换参数。
Snapshot 的边界决定离线客户端能否回来
Export Mode 区分操作增量、完整 Snapshot 和 Shallow Snapshot。完整 Snapshot 保存已整合的历史与状态;Shallow Snapshot 截断保留起点之前的历史,同时保留后续恢复所需状态。减少存储体积的代价是改变了能够接纳的历史范围。
假设服务端清理历史时,有个笔记本仍离线在旧版本上修改。它带回的更新可能与 Shallow Snapshot 的起点并发,不能指望直接导入。设计清理策略时,必须一起设计旧副本重入:先保护未同步修改,再选择补历史、迁移、人工合并或重新建立副本,不能用“重置客户端”掩盖数据损失。
另一个更隐蔽的故障来自乱序:先收到依赖 A 的 B,A 尚未到达,B 进入 pending。当前官方 Persistence 明确说明,普通 Snapshot 不含这些等待因果前驱的导入操作。此时若保存 Snapshot 后就删除 B 的原始字节,服务重启会丢掉已经收到但未整合的 B。
因此,检查点应保留原始待整合字节,或采用所用版本支持的 pending-import 导出机制,并把状态与 pending 数据作为一致的一代持久化。本文不把当前文档中的新增 API 假定为所有历史版本可用;升级前应核对实际包与恢复测试。
有 loro-prosemirror,不代表可以只换一个包
loro-prosemirror 提供文档同步、协作撤销和光标相关接线,但不是 y-prosemirror 的二进制兼容实现。当前 README 优先介绍 EphemeralStore 光标方案,同时提到旧 Awareness / Cursor 路线;不宜跨版本复制初始化片段。
把 Yjs 改为 Loro,至少要同时核对 Binding 支持的节点、更新编码、Provider 协议、存储、presence、撤销和旧数据迁移。Tiptap 可以加载 ProseMirror 插件,并不表示所有依赖 Yjs 类型的扩展都能随之替换。导出可见正文再重建文档,也不等于保留了原协作身份与全部历史。
房间服务:PartyServer、y-partyserver 与 Hocuspocus
“一个文档一个房间”是常见组织方式。房间知道谁连接了哪个文档,负责加载、广播、断开及恢复,但房间本身不是 CRDT。Cloudflare Durable Object(下文简称 DO)提供执行、协调与存储能力,也不会自动把一份编辑器 JSON 变成协作文档。
| 项目 | 所在职责层 | 使用时要额外确认 |
|---|---|---|
| PartyServer | 基于 DO 的连接、房间与生命周期 | 文档授权、恢复和业务消息协议 |
| y-partyserver | 在 PartyServer 上组织 Yjs server、Provider 与保存接线 | 加载/保存钩子的实际语义、只读写入控制 |
| Hocuspocus | Yjs Provider / server,含认证和扩展接线 | 存储实现、运行适配与确认时点 |
当前 Cloudflare PartyKit 仓库 以 PartyServer 为核心;y-partyserver README 展示 YServer 和加载、保存接口。旧 y-partykit 的配置属于另一代 API,不能因旧示例有 readOnly 就推断当前接线已经阻止所有写入。只读必须在真正接收文档更新的控制点验证。
Hocuspocus Overview 则把它定位为 Yjs 协作后端及 Provider;当前概览说明 v4 起有 Cloudflare Workers 支持。因此“只能跑 Node.js”不是跨版本成立的结论。具体 adapter、扩展和数据库驱动能否运行在目标环境中,仍要逐项核对,而不是由项目名称推断。
“已保存”应该是哪一级承诺
编辑器内容已经变化、IndexedDB 写入完成、WebSocket 已发送、服务端已接收、数据库已持久化、搜索索引已更新,这是六个不同事件。界面只画一个绿色勾号时,产品必须先选定它代表哪件事。
下面是一个应用自定义确认协议的示意,不是 Yjs 或 Loro 内置 API。假定服务器在持久存储提交成功后才确认对应批次,客户端只把已确认的本地修改范围标为保存;确认过程中继续输入的新内容仍是待保存。
sequenceDiagram
participant E as 编辑器 / CRDT
participant P as Provider
participant R as 房间服务
participant D as 持久存储
E->>P: 本地 update 与待确认批次
P->>R: 发送 update
R->>R: 校验身份、文档权限与协议
R->>D: 持久化可恢复的数据
alt 提交成功
D-->>R: 提交结果
R-->>P: 应用层持久化确认
P-->>E: 仅确认该批次,保留后续待保存项
else 失败或确认丢失
P->>R: 重连后补齐 / 重试
Note over E,D: 保留未确认修改,恢复过程应支持重复投递
end
这个设计有几个实际后果。首先,收到 update 的回调不能直接点亮“已保存”;异步保存可能稍后失败。其次,确认丢失时可以重复投递,但应用附带的副作用也要幂等,不能因 CRDT update 幂等,就重复创建附件记录或审计事件。最后,服务端恢复时必须能从持久数据重建已确认的修改,不能依赖某个房间恰好还没退出的内存。
检查点与产品版本分开管理
恢复检查点可以由完整状态加后续更新组成,也可以按引擎格式保存。生成新检查点后,应先保证它及所需 pending 数据已可靠提交,再裁剪被覆盖的日志,并确保切换期间的新增修改没有漏掉。
而用户点击“发布第 3 版”,通常还需要不可变版本 ID、Schema 版本、附件引用、作者、发布时间和保留策略。CRDT Snapshot 或一个版本边界,不会自动保存这些业务约束;只保存边界却删掉它依赖的历史,也可能无法重建。
“恢复到旧版”尤其需要明确语义:是把旧内容作为当前文档的一次新修改,还是创建独立文档?直接把旧文件覆盖进活跃房间,可能与在线用户的未确认修改冲突。产品版本、协作撤销、底层历史浏览,是三种不同能力。
上线前最容易遗漏的三类规则
身份与 ACL 不能藏在编辑器里
服务端应将已验证身份与文档、租户和角色绑定;房间名、Yjs clientID、Loro peerID、transaction origin、Awareness 展示字段都不能代替认证。读权限决定能否取得文档数据,写权限决定能否导入更新;禁用工具栏只是 UI 行为。
连接建立后权限还可能变化。需要定义撤权、token 过期、重连和离线修改回传时的处理方式,并限制消息大小、频率与资源消耗。CRDT 能合并数据,并不替服务器判断这个更新是否应该被接受。
空文档不等于新文档
两个客户端同时连接,都在加载完成前看见空内容,然后各自插入“会议模板”。CRDT 可能很正确地合并成两份模板。问题在初始化竞态,不在合并算法。
初始化应在具有唯一性保障的创建流程中完成,或由房间/存储的原子判定协调;客户端要区分“尚未加载”“真实为空”“已经初始化”。浏览器本地缓存、服务端历史和初始 JSON 同时存在时,也要确定谁是恢复依据。仅在 mount 时检查一次空树,不足以建立这个保证。
schemaVersion 和未知节点是升级协议
客户端 A 新增了一个“决策卡片”节点,旧客户端 B 只认识段落。如果 B 在导入时把未知节点删掉,再把修复结果同步回去,就可能造成持续的数据损坏。
schemaVersion 应参与文档元数据和接入检查。遇到未知节点,可以阻止写入并要求升级;也可以在确认能无损保留原始结构时用占位展示。降级成纯文本只有在语义损失被明确接受后才成立,不能悄悄做。迁移时还要覆盖 HTML / Markdown 导出、评论锚点、历史版本及旧离线客户端重入,不只是让新客户端成功打开一次。
性能与运行环境:不要把 Headless 理解成哪里都能跑
浏览器中的编辑视图仍要处理 DOM、Selection、布局与 IME。CRDT 编解码、搜索等工作有时适合放入 Web Worker,但把整个编辑器实例搬过去通常不成立:Worker 没有页面 DOM,消息传递还会增加复制和调度成本。
服务端也要区分“不显示 UI”和“不依赖 DOM”。某个转换函数能在 Node.js 中借助环境适配运行,不代表它在 workerd 中可直接使用。选择 DO 房间时,还要核对内存生命周期、WebSocket hibernation、恢复加载以及所用包的运行时依赖;不能把进程内对象当成持久数据库。
性能测试应按用户动作拆开:长文输入延迟、粘贴大段内容、远端更新应用、冷启动恢复、离线积压追赶、检查点体积与内存峰值。Tiptap Performance 也强调隔离编辑器与不必要的 React 重渲染。比较两个方案时,需要固定文档、历史规模、在线人数和网络条件;本文没有做这类跨库基准,不给性能排名。
用故障恢复检验选型,而不只看演示
选完内核与框架后,可以把下面这些场景变成集成测试。它们是验收建议,不是本文已经替某个生产组合跑过的结果。
| 场景 | 需要检查的结果 |
|---|---|
| 两个副本离线修改后重连,更新乱序并重复投递 | 最终正文收敛;重试不重复产生业务副作用 |
| A 输入后 B 插入,再由 A 撤销 | 撤销范围符合产品规则,不把 B 的内容整体覆盖 |
| 服务端确认前后分别崩溃并重启 | 已确认修改可恢复;未确认修改可安全重试 |
| 两个客户端同时打开新文档 | 模板只创建一次,不把未加载误判为空 |
| 旧客户端打开带新节点的文档 | 按兼容策略拒绝写入或无损保留,不静默删除 |
| 历史裁剪后,长期离线副本携未同步修改回来 | 明确识别边界,保护修改并走恢复/迁移流程 |
| Loro 接收缺少前驱的更新后保存、重启 | pending 更新仍可在前驱到达后整合 |
| 在线连接被撤销写权限 | 后续更新确实被服务端拒绝,而不只是 UI 变灰 |
正文模型决定可表达什么,Binding 决定如何把编辑映射为协作操作,Provider 决定如何交换信息,存储协议决定何时可以承诺已保存。选型时把这些问题分开,再用断线、重试、重启和升级测试把它们连起来,比只看一次双人编辑演示更能判断方案是否适合自己的产品。
公开资料
资料核查日期:2026-10-28。整理线索中的 Context7 检索标识为 /ueberdosis/tiptap-docs、/yjs/docs、/websites/loro_dev;它们不是 npm 版本。本轮尝试 Context7 时返回月度配额耗尽,未获得检索结果,以下陈述改由官方文档和公开源码核对,不把既有检索线索写成本轮成功调用。
| 资料 | 本文核对主题 |
|---|---|
| ProseMirror Guide / Collab | Schema、Transaction、Step、中心顺序与 rebase |
| Slate Operations / Normalizing | 操作和结构修复的关系 |
| Lexical Editor State | 状态读写、提交和序列化边界 |
| Tiptap Introduction / Collaboration | ProseMirror 基础、扩展与协作撤销接线 |
| Yjs Document Updates / Editor Bindings | update、State Vector、模型适配 |
| Yjs Awareness / UndoManager | 临时状态、撤销范围与 origin |
| y-prosemirror v1.3.7 | 该发行版的 Y.XmlFragment 映射与插件职责 |
| Loro OpLog / DocState / Version Deep Dive | 历史、物化状态、Version Vector 与 Frontiers |
| Loro Encoding / Persistence | Shallow Snapshot 边界与 pending-import 恢复 |
| loro-prosemirror | 同步、光标、撤销,不等同于替换整个协议栈 |
| PartyServer / y-partyserver / Hocuspocus | 房间、Provider、存储接线与运行环境 |
文中固定版本链接只为说明对应实现,不是最新安装建议;其余页面会继续演进。将机制用于具体产品前,仍应固定依赖并执行自己的兼容性与恢复测试。