协同编辑必须采用操作指令同步而非文档快照,核心是基于单调递增seq的因果序广播与客户端严格序号驱动的应用逻辑,否则必然导致错位、乱序与不一致。

协同编辑不是靠“同步文档快照”,而是靠“同步操作指令”——发错一条 insert 操作,所有客户端就可能错位、崩溃或卡死。
为什么不能直接广播 HTML 或 JSON 文档全量内容
全量同步看似简单,实际在多人编辑中会迅速暴露问题:网络抖动导致旧版本覆盖新版本、光标位置丢失、折叠状态错乱、输入法组合字符被截断。更关键的是,它无法解决“两个用户同时在第 5 行插入文字”的根本冲突。
真实生产环境(如 Luckysheet、Monaco 集成场景)全部采用操作粒度同步,典型消息结构必须包含:type(如 "insert"、"delete")、position(带行列/索引上下文)、text 或 value、clientId 和单调递增的 seq。
- 服务端收到
{type: "insert", row: 4, col: 12, text: "x=", seq: 105}后,不修改自身文档副本,只校验seq是否连续、row/col是否越界 - 校验通过即广播,**不做 OT 变换也必须保证广播顺序与接收顺序一致**——这点常被忽略,却直接决定最终一致性
- 前端编辑器必须提供原子 apply 接口(如 Luckysheet 的
luckysheet.setCellValue、CodeMirror 6 的view.dispatch),否则本地状态会漂移
WebSocket 连接管理必须绑定业务上下文
单纯用 setAllowedOrigins("*") 或全局 active_connections 列表,会在真实协作中引发权限越界和消息泄露。
每个连接必须携带且验证以下上下文:
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
-
worksheet_id或doc_id:用于路由,避免 A 用户编辑的表格消息推送给 B 用户打开的文档 -
user_id+role("viewer"/"editor"):服务端在广播前过滤——学生发来的delete操作,教师未授予权限时应直接丢弃,而非转发后由前端拦截 -
client_seq初始值:由服务端在accept()后下发,客户端后续每条消息附带该值+自增,服务端据此识别重连后的状态断点
FastAPI 示例中,别写 @app.websocket("/ws") 这种泛化路径;必须是 @app.websocket("/ws/{doc_id}"),并在 websocket_endpoint 函数内校验 doc_id 对应的团队空间权限和用户角色。
seq 序号不是可选字段,而是去重与因果排序的唯一依据
没有 seq 的 WebSocket 协同系统,在弱网或重连场景下必然出现操作乱序、重复应用、光标跳变。它不是日志序号,而是操作因果链的锚点。
正确做法:
- 客户端每次发送操作前,本地
seq += 1,并随消息体发出(不要依赖服务端生成) - 服务端维护每个
clientId的最新已收seq,收到seq ≤ 已收的消息直接丢弃(防重传) - 广播时**不重排**,但要求客户端按
seq严格排队应用:若收到seq: 107而本地只到105,缓存该消息,等待106到达后再批量执行 - 避免用
timestamp替代seq:多设备时钟不同步,毫秒级误差即可导致因果颠倒
最易被跳过的细节是:前端必须实现操作队列缓冲与严格序号驱动的应用逻辑,而不是“收到就 apply”。一旦漏掉这个环节,协同就退化为竞态编辑——表面实时,实则不可靠。

















