技术方案 · V1.0
多人在线协同编辑 Markdown 文件方案
基于 WebSocket + CRDT 的实时协同编辑能力设计,支持多人同时编辑、冲突自动合并、光标实时同步与细粒度权限控制
文档类型:方案设计
适用范围:网页端 / 桌面端
日期:2026 年 9 月
为什么需要协同编辑,以及本方案要达成的目标
1.1 背景
当前团队基于 Markdown 进行知识沉淀(方案文档、周报、会议纪要、知识库条目等),文件以本地或单点存储为主,存在以下痛点:
- 串行协作效率低:同一文档需轮流编辑,通过 IM 传文件或"等对方改完再改",来回沟通成本高;
- 版本冲突频发:多人各自持有副本,合并时出现内容覆盖与丢失;
- 状态不透明:无法感知"谁正在看、谁正在改哪一段",协作缺少上下文;
- 评审低效:评审意见散落在 IM 与批注中,难以与文档内容精确对应。
1.2 目标
| 目标 | 说明 |
| 实时协同 | 多人同时编辑同一 Markdown 文件,内容变更端到端延迟控制在 300ms 以内(同机房) |
| 无感合并 | 并发编辑自动合并,无需人工处理冲突;保证所有客户端最终一致 |
| 在线状态 | 实时展示协作者头像、光标位置、选区范围;支持关注(Follow)模式 |
| 权限可控 | 支持只读 / 评论 / 可编辑三级权限,按文档(后续可扩展到段落级)授权 |
| 历史可溯 | 保留操作级历史,支持任意时刻快照回滚与按人的变更回放 |
非目标(本期不做)
不做富文本块编辑器(保持纯 Markdown 语法语义);不做离线编辑(要求在线使用);不做跨文档引用渲染(仅渲染本文件内容)。
协同编辑的本质难题是"并发"——必须先想清楚难在哪,再谈方案
- 并发冲突:两人同时在不同位置插入/删除文本,服务端必须按统一规则合并,且结果对所有人生效一致(收敛性)。
- 意图保留:A 在第 5 行插入内容后,B 基于旧视图的"第 8 行删除"操作要正确作用于新文档,而非错位删除。
- 网络不可靠:断线、弱网下编辑不能丢;重连后需快速追平差异(增量同步,而非全量拉取)。
- 光标同步:光标/选区是位置型数据,随文档变化漂移,需与文本操作用同一套坐标变换体系。
- 性能与规模:单文档协同人数需支撑 20~50 人不卡顿;大文档(> 100KB)的算法开销需可接受。
客户端编辑器 + WebSocket 长连接 + 协同服务 + 持久化存储的四层结构
3.1 分层职责
| 层次 | 职责 | 关键技术 |
| 客户端层 | Markdown 编辑与渲染、本地 CRDT 文档状态、光标/在线状态广播、断线缓存重放 | CodeMirror 6、Yjs、ProseMirror 可选 |
| 协同服务层 | 文档房间管理、消息转发、增量合并与广播、防抖持久化、权限校验 | Node.js、ws、y-websocket、Redis Pub/Sub |
| 存储层 | 增量更新存储、定期快照合并、版本历史、元数据与权限、附件 | MySQL、对象存储、Redis |
3.2 房间(Room)模型
每个打开中的 Markdown 文件对应一个协同房间:
- 房间由
docId 唯一标识,服务端为每个房间维护一份 Yjs 文档实例(内存态);
- 客户端进入房间即建立连接并做全量/增量同步,退出房间后服务端延迟回收内存实例;
- 无活跃连接且已落盘的房间被回收,再次打开时从快照恢复。
OT 与 CRDT 两条主流路线的对比与本方案的取舍
| 维度 | OT(Operational Transformation) | CRDT(如 Yjs) |
| 合并职责 | 依赖中心服务器做变换,客户端逻辑简单 | 各端基于本地操作独立收敛,服务器仅转发 |
| 实现复杂度 | 服务器变换函数难以写对,历史缺陷多 | 算法由成熟库封装(Yjs),业务侧开发量小 |
| 离线/弱网 | 依赖稳定的服务器序列,离线支持复杂 | 天然支持:本地先生成 op,上线后自动合并 |
| 光标同步 | 需另建坐标变换机制 | Relative Position 原生支持,与文档同源 |
| 性能 | 大文档、长历史时变换开销增长明显 | Yjs 增量编码 + 快照压缩,实测百万字符级可用 |
| 典型代表 | Google Docs(自研)、ShareDB | Yjs、Automerge、Loro |
选型结论:采用 CRDT 路线,具体使用 Yjs
① 我们的场景是中小团队文档协作,不追求 Google Docs 级自研 OT;② Yjs 与 CodeMirror 6 有官方成熟绑定,接入成本低;③ 服务器只做"转发 + 存储",复杂度低、易水平扩展;④ 增量协议天然支持断线追平与历史回放。
4.1 Yjs 工作原理(简要)
- 每个字符/块插入被编码为带唯一 ID(client + clock)的 Operation,插入操作记录左邻节点,形成链式结构;
- 并发插入时按
clientID 等规则确定性排序,所有端无需通信即可得到相同结果(收敛性由算法保证);
- 删除操作只是"打墓碑标记",不物理移除节点;
- 更新以 Update(二进制增量)形式在端间传播;服务端定期将增量合并为快照(
Y.encodeStateAsUpdate)压缩存储。
4.2 并发场景行为约定
| 并发场景 | 合并结果 |
| 两人同时在不同位置插入文本 | 两处插入均保留,按确定顺序排列 |
| 同一位置并发插入 | 按客户端 ID 决定先后,结果稳定一致 |
| 一人编辑、一人删除同一段落 | 删除生效(删除优先),编辑内容保留在墓碑层,历史中可查 |
| Markdown 语法结构(列表、表格) | 按纯文本行处理;行级整体移动为"删 + 插",不保证特殊结构语义合并 |
注意
本方案将 Markdown 视为纯文本进行 CRDT 同步(Y.Text),不做块级(Y.Map / Y.Array)结构化建模。优点是接入简单、与现有纯 Markdown 文件完全兼容;代价是列表/表格的结构语义合并较弱。若未来演进为块编辑器,可切换 Yjs 嵌套类型(Y.XmlFragment),本方案架构不变。
编辑器、同步协议、在线状态、权限、历史五个模块的设计要点
5.1 编辑器模块(客户端)
| 子项 | 设计 |
| 编辑内核 | CodeMirror 6:轻量、插件体系清晰,官方提供 y-codemirror.next 绑定,自动完成"本地编辑 → Yjs Update → 远端回放"闭环 |
| 双栏视图 | 左栏 Markdown 源码编辑、右栏实时渲染预览;渲染层可跟随光标滚动同步 |
| 远端光标 | 通过 yCollab 的 Awareness 能力,为每个协作者渲染带名字标签的彩色光标与选区;颜色按 userId 哈希固定 |
| 本地缓冲 | Yjs Update 先写入本地内存并即时上屏,网络失败进入待发队列,重连后按序补发——保证"本地永不丢键" |
5.2 同步协议(y-websocket)
采用 Yjs 官方 y-websocket 二进制协议,消息类型:同步请求(SyncStep1/2)、增量更新(Update)、在线状态(Awareness)。
客户端接入流程:
1. WS 连接 /ws/doc/:docId?token=xxx
2. 服务端校验 token 与文档权限 → 绑定用户与房间
3. 双方互发 SyncStep1(各自持有状态向量 SV)
4. 对方按差集回传 SyncStep2(缺失的增量 Update)
5. 此后本地编辑实时广播 Update;Awareness 心跳 15s/次
- 增量同步:状态向量(State Vector)机制保证只传输对方缺失的部分,重连追平开销极小;
- 消息节流:本地编辑产生的高频 Update 在客户端做 50ms 微批,降低广播风暴;
- 心跳保活:30s 服务端心跳,90s 无响应判定离线并清理 Awareness。
5.3 在线状态模块(Awareness)
| 信息 | 说明 |
| 用户身份 | userId、昵称、头像 URL、主题色 |
| 光标 / 选区 | Yjs Relative Position 锚点,随文档演进自动重定位 |
| 视图状态 | 正在编辑 / 正在预览 / 空闲(超过 60s 无操作标记为空闲) |
顶部协作栏实时展示在线成员头像列表;提供「关注某人」能力——跟随目标用户的光标与滚动位置,便于结对评审与讲解。
5.4 权限模块
| 角色 | 能力 | 实现方式 |
| 只读 | 查看内容与在线成员 | WS 连接标记只读,服务端拒绝其 Update 消息;客户端编辑器置为 readOnly |
| 评论 | 只读 + 行级评论 | 评论走独立 HTTP 接口与数据表,不进入 CRDT 文档 |
| 可编辑 | 全部能力 | 正常参与协同 |
- 鉴权规则(文档归属 → 用户角色)缓存在服务端房间内存中,权限变更通过 Redis 广播实时生效(踢出或降级连接);
- 评论以「行号 + 内容 + 作者 + 时间」存储,配合 Yjs 的行稳定性问题(行号随编辑漂移),评论锚点同样使用 Relative Position 定位到文本锚而非行号。
5.5 历史版本模块
- 增量存储:服务端将收到的 Update 追加写入
doc_updates 表(docId, seq, update, clientId, createTime);
- 定期快照:每 5 分钟或累计 500 条增量时合并一次快照写入
doc_snapshots,并压缩已合并增量;
- 版本查看:历史面板按时间线列出快照 + 增量,可回放至任意时刻;对比视图高亮"谁在何时改了什么";
- 回滚:回滚=将历史版本内容作为一次新编辑写入当前文档(生成新增量),保证其他在线用户实时看到回滚结果,而非粗暴覆盖。
5.6 持久化策略
落盘时机(防抖 + 兜底):
- 编辑静默 3 秒后,将服务端房间 Y.Doc 当前状态编码落盘
- 最后一名用户离开房间时强制落盘并回收
- 每 5 分钟定时快照一次(兜底)
- 服务优雅停机前 flush 全部活跃房间
恢复路径:启动加载最新快照 → 重放快照之后的增量 Update → 得到完整文档状态。
各关键组件的候选对比与最终选择
| 组件 | 候选方案 | 最终选择 | 理由 |
| CRDT 库 | Yjs / Automerge / Loro | Yjs |
生态最成熟(编辑器绑定齐全)、性能标杆、二进制 Update 紧凑 |
| 编辑器 | CodeMirror 6 / Monaco / ProseMirror | CodeMirror 6 |
专为轻量源码/文本设计,体积小、移动端表现好,Yjs 官方绑定 |
| 通信 | WebSocket / SSE / WebRTC | WebSocket |
双向低延迟;SSE 单向不适合;WebRTC 仅在超低延迟场景有必要 |
| 服务端 | Node.js / Go / Java | Node.js |
y-websocket 官方实现开箱即用;I/O 密集型场景表现好 |
| 渲染 | marked / markdown-it / unified | markdown-it |
CommonMark 兼容好、插件生态丰富、可扩展代码高亮与任务列表 |
自研 vs 集成现成产品
若允许引入第三方,可评估飞书文档、语雀、Notion 等托管能力(快、稳,但数据不出自己体系、Markdown 兼容受限)。本方案定位自建轻量协同能力:核心链路基于开源组件拼装(y-websocket + CodeMirror),自研部分集中在权限、历史与业务集成,预计核心功能 3~4 周可交付 MVP。
一次典型协同编辑的端到端流程
7.1 断线重连流程
- WS 断开后客户端指数退避重连(1s → 2s → 4s,上限 30s),期间编辑持续写入本地 Y.Doc;
- 重连成功后互发 SyncStep1,服务端按状态向量只回传缺失增量,本地待发队列补发离线期间的编辑;
- 重连期间顶部提示条展示「连接已断开,正在重连…」,恢复后自动消失。
从单机到集群的演进路径
| 阶段 | 形态 | 要点 |
| MVP | 单机部署:1 个 Node 进程 + MySQL + Redis | 房间全在单进程内,无需跨节点广播;预计支撑 50+ 并发房间、单房间 20 人 |
| 水平扩展 | 多进程 / 多机 + Redis Pub/Sub | 同一房间由一致性哈希路由到固定节点;Redis Pub/Sub 负责跨节点的 Awareness 与控制消息广播 |
| 高可用 | Nginx/网关负载均衡 + 健康检查 | 节点故障时房间在其他节点从快照重建;WS 网关配置 sticky session |
8.1 容量估算(MVP 假设)
- 单房间 20 人、每人平均 2 次编辑/秒 → 每房间约 40 msg/s,单进程(Node)可承载 100+ 房间的转发;
- Update 消息经微批后单条 100~500B,百房间峰值带宽 < 20Mbps,网络不是瓶颈;
- 存储:1 万篇文档 × 平均 20KB,快照 + 增量总和预估 < 2GB/年,MySQL 单表足够,按 docId 分区。
8.2 安全与运维
- 传输安全:WSS(TLS)加密链路;token 采用短期 JWT(30min),过期前由客户端静默续签;
- 输入安全:预览渲染统一 sanitize(防 XSS),图片外链走代理校验;
- 限流:单连接 Update 频率上限(如 100 msg/s),超限合并或丢弃重传,防恶意刷包;
- 监控:房间数、在线人数、消息 QPS、端到端延迟 P95、落盘成功率,接入现有告警通道(企业微信机器人)。
三个阶段递进交付,每阶段均有可演示产出
| 阶段 | 周期 | 交付内容 | 里程碑验收标准 |
| 一期 · MVP | 第 1~3 周 |
编辑器双栏界面、y-websocket 协同服务、多端实时同步、远端光标、基本落盘与恢复 |
两浏览器并发编辑完全一致;刷新/重启服务后内容不丢 |
| 二期 · 协作增强 | 第 4~5 周 |
三级权限、行级评论、历史版本查看与回滚、断线重连体验优化 |
权限变更实时生效;可回放任意历史时刻并安全回滚 |
| 三期 · 工程化 | 第 6~8 周 |
多节点扩展(Redis Pub/Sub)、监控告警接入、压测与调优、与知识库/现有系统集成 |
单房间 50 人压测 P95 延迟 < 500ms;服务重启无数据丢失 |
9.1 风险与对策
| 风险 | 影响 | 对策 |
| 纯文本 CRDT 对 Markdown 结构合并较弱 | 列表/表格并发编辑语义不理想 | 一期按纯文本交付并在文档中说明边界;三期评估 Y.XmlFragment 块级建模升级 |
| 大文档性能(>100KB) | 首屏同步慢、内存占用高 | 压测验证;必要时按章节分段加载 / 虚拟滚动 |
| 多人高频编辑消息风暴 | 延迟抖动 | 客户端微批 + 服务端合流广播;限流兜底 |
| 与现有文件存储体系不一致 | 协同态与磁盘文件状态出现分歧 | 以协同文档为唯一真源,落盘单向写回 Markdown 文件;避免双写冲突 |
方案小结
以 Yjs(CRDT)+ CodeMirror 6 + Node WebSocket 服务为核心搭建轻量协同编辑能力:算法层保证并发自动合并与最终一致,架构层以"房间 + 增量同步 + 防抖快照"支撑多人文档协作,业务层叠加权限、评论、历史三大协作要素。整体基于成熟开源组件,自研范围可控,8 周内可完成三阶段交付。