WBO 2.18.31 常用操作与 API 详解
WBO 2.18.31 常用操作与 API 详解:HTTP 端点与 Socket.IO 协议实测手册,覆盖消息契约(MutationType/工具编码/字段范围)、双端同步回显、mutation_rejected 拒因与 baseline_not_replayable 重连实测。
WBO 2.18.31 常用操作与 API 详解
本文是 WBO 专栏的接口篇。全部端点、事件与消息契约均在 2.18.31 实例上用
curl与socket.io-client@4.8.4实测走通,回显为实测原文(节选处已标注)。组件画像见《WBO 2.18.31 介绍与选型指南》,部署见《WBO 2.18.31 Docker Compose 部署指南》,限流与回放参数的调优见《WBO 2.18.31 配置参数详解》。
一、环境信息
| 项目 | 取值 |
|---|---|
| 实测版本 | WBO 2.18.31(服务端依赖 socket.io ^4.8.4) |
| 测试端 | Node.js ≥ 22.23.4 + socket.io-client@4.8.4(npm install socket.io-client 实测 4 秒装完) |
| 服务地址 | http://127.0.0.1:80(默认 PORT 口径,按实际部署端口替换) |
| 实测时间 | 2026-10-30 |
二、接口概览
2.18.31 的对外接口分两层:
| 层 | 端点/事件 | 用途 |
|---|---|---|
| HTTP | GET / | 首页(随机板入口) |
| HTTP | GET /boards/<板名> | 板页,访问即隐式建板 |
| HTTP | 静态资源(/index.css 等) | 前端资源 |
| WebSocket | socket.io 挂载于 /socket.io,查询参数 board=<板名> | 实时协作通道(读写均走这里) |
| WebSocket | 事件 broadcast(双向) | 写操作上行 / 同步与回放下行 |
💡 技术拓展(Socket.IO 与裸 WebSocket 的取舍):WBO 选 Socket.IO 而非裸 WebSocket,换来的是自动重连、降级回退与基于回调的事件路由;代价是协议封包(engine.io 握手 + 心跳),调试时不能用
wscat直连,需要 socket.io-client 或支持 engine.io 协议的工具。HTTP API 侧注意:旧版 WBO 的/export、/history服务端端点在 2.18.31 已移除(实测分别返回 400/404),导出与历史回放全部走前端与 WebSocket,脚本化集成不要再去打这两个 URL。
三、前置条件
- 一套已运行的 WBO 实例(部署见 B2);
- 测试机安装 Node.js ≥ 22.23.4,并安装客户端:
npm install socket.io-client@4.8.4
added 9 packages in 4s
- 如需观察服务端行为,
LOG_LEVEL可临时切debug(见 C1 5.1)。
四、HTTP 端点详解
4.1 首页
curl -s http://127.0.0.1:80/ | head -c 400
<!DOCTYPE html>
<html lang="en" dir="ltr" data-wbo-version="2.18.31">
<head>
<meta charset="utf-8" />
<title>WBO — Collaborative whiteboard</title>
<base href="http://localhost:80/" />
<link rel="stylesheet" href="index.css" />
<meta name="description" content="A free and open-source online collaborative drawing tool. Sketch new ideas together on WBO!" />
data-wbo-version 是确认版本口径的最快方式;<base href> 说明前端资源按绝对基址解析,反代改写时留意该头的正确性。
4.2 板页(隐式建板)
curl -s http://127.0.0.1:80/boards/demo | head -c 240
<!DOCTYPE html>
<html lang="en" dir="ltr" data-wbo-version="2.18.31" data-ui-direction="ltr">
<head>
<meta charset="utf-8" />
<title>demo | WBO | Collaborative whiteboard</title>
访问任意合法板名即创建板,无需预注册;标题即 板名 | WBO | Collaborative whiteboard。板名经规范化校验(normalizeBoardName),非法板名连不上 WebSocket(invalid_board_name)。
4.3 已移除端点的行为
curl -s -o /dev/null -w 'export=%{http_code}\n' 'http://127.0.0.1:80/export?board=demo'
curl -s -o /dev/null -w 'history=%{http_code}\n' 'http://127.0.0.1:80/history?board=demo'
export=400
history=404
两者都返回统一错误页(An unknown error occurred),确认无服务端导出/历史端点可用。
五、WebSocket 协议详解
5.1 连接与初始事件
连接方式(socket.io-client):
import { io } from "socket.io-client";
const socket = io("http://127.0.0.1:80", { query: { board: "demo" } });
连入后服务端依次推送(实测回显,节选):
[user_joined] {"socketId":"pFU0BFa9qDZCsM9pAAAG","userId":"zaweto","name":"feme zaweto","color":"#001f3f","size":10,"lastTool":"hand","joinedAt":1793320350327,"position":{"x":0,"y":0},"canEdit":true,"canClear":false,"canBan":false,"canGrantTemporaryModerator":false}
[boardstate] {"readonly":false,"canEdit":true,"canClear":false,"canWrite":true,"canBan":false,"canGrantTemporaryModerator":false,"canReport":true}
[broadcast] {"type":5,"fromSeq":0,"seq":0,"_children":[]}
user_joined:成员档案(随机昵称name、代表色color、能力位);boardstate:本连接的能力清单(只读/可写/可清板/可封禁等),程序化集成应以它判断可用操作;- 首个
broadcast:回放批次信封,type:5是 BATCH 类型,新连接从序号 0 起步时_children为空数组,随后增量操作逐条推送。
服务端事件速查:
| 事件 | 方向 | 说明 |
|---|---|---|
boardstate | S→C | 连接能力清单 |
user_joined / user_left | S→C | 成员进出 |
broadcast | 双向 | C→S 上行写操作;S→C 同步/回放 |
mutation_rejected | S→C | 写操作被校验拒绝(含 reason 与 clientMutationId) |
rate-limited | S→C | 触发限流 |
active_bans | S→C | 当前封禁名单 |
moderation_disconnect | S→C | 被版主强制断开 |
5.2 写操作消息契约
上行消息挂在 broadcast 事件上,type(MutationType)与 tool(工具编码)均为数字:
MutationType 编码表(实测源码 mutation_type.js):
| 值 | 类型 | 用途 |
|---|---|---|
| 1 | CREATE | 新建元素 |
| 2 | UPDATE | 更新字段(如文本内容) |
| 3 | DELETE | 删除元素 |
| 4 | APPEND | 追加子项(画笔加拐点) |
| 5 | BATCH | 批量信封(服务端回放批次) |
| 6 | CLEAR | 清板 |
| 7 | COPY | 复制 |
工具编码表(实测源码 tools/manifest.js):
| 值 | 工具 | 值 | 工具 |
|---|---|---|---|
| 1 | pencil(画笔) | 7 | hand(视角) |
| 2 | straight-line | 8 | grid(网格) |
| 3 | rectangle | 9 | download(导出) |
| 4 | ellipse | 10 | zoom |
| 5 | text(文本) | 11 | clear(清板) |
| 6 | eraser(橡皮) | 12 | cursor(光标) |
各工具的字段契约(liveMessageFields 实测原文):
| 工具 | type | 必填字段 |
|---|---|---|
| pencil(1) | 1 CREATE | id、color、size、opacity? |
| pencil(1) | 4 APPEND | parent(指向路径 id)、x、y |
| text(5) | 1 CREATE | id、color、size、opacity?、x、y |
| text(5) | 2 UPDATE | id、txt |
| eraser(6) | 3 DELETE | id |
| clear(11) | 6 CLEAR | 无 |
字段取值硬限制(排障速查):画笔/形状线宽 size ∈ [10, 500]、opacity ∈ [0.1, 1]、文本 ≤ 280 字符、id ≤ 128 字符、坐标受 WBO_MAX_BOARD_SIZE 约束。建议每条上行消息带 clientMutationId(客户端生成),被拒时服务端按该 id 回执,便于本地重试/回滚。
5.3 实测:画一笔并双端同步
完整实测脚本(两个客户端 A/B 连同一板,A 画笔 CREATE + 三次 APPEND):
import { io } from "socket.io-client";
const URL = "http://127.0.0.1:80";
const a = io(URL, { query: { board: "demo" } });
const b = io(URL, { query: { board: "demo" } });
a.on("connect", () => setTimeout(() => {
a.emit("broadcast", { type: 1, tool: 1, id: "pen-f1", color: "#0044cc", size: 12, clientMutationId: "f1" });
setTimeout(() => a.emit("broadcast", { type: 4, tool: 1, parent: "pen-f1", x: 120, y: 130, clientMutationId: "f2" }), 250);
setTimeout(() => a.emit("broadcast", { type: 4, tool: 1, parent: "pen-f1", x: 280, y: 260, clientMutationId: "f3" }), 500);
setTimeout(() => a.emit("broadcast", { type: 4, tool: 1, parent: "pen-f1", x: 420, y: 180, clientMutationId: "f4" }), 750);
}, 800));
["broadcast", "mutation_rejected"].forEach(ev => a.on(ev, (...x) => console.log("[A]", ev, JSON.stringify(x))));
["broadcast", "mutation_rejected"].forEach(ev => b.on(ev, (...x) => console.log("[B]", ev, JSON.stringify(x))));
A、B 双端回显完全一致(实测原文,节选):
[B] broadcast {"seq":1,"acceptedAtMs":1793320488544,"mutation":{"tool":1,"type":1,"id":"pen-f1","color":"#0044cc","size":12,"clientMutationId":"f1","socket":"mYsQluXvLIgbzzNZAAAM"}}
[A] broadcast {"seq":1,"acceptedAtMs":1793320488544,"mutation":{"tool":1,"type":1,"id":"pen-f1","color":"#0044cc","size":12,"clientMutationId":"f1","socket":"mYsQluXvLIgbzzNZAAAM"}}
[B] broadcast {"seq":2,"acceptedAtMs":1793320488795,"mutation":{"tool":1,"type":4,"parent":"pen-f1","x":120,"y":130,"clientMutationId":"f2","socket":"mYsQluXvLIgbzzNZAAAM"}}
[A] broadcast {"seq":2,"acceptedAtMs":1793320488795,"mutation":{"tool":1,"type":4,"parent":"pen-f1","x":120,"y":130,"clientMutationId":"f2","socket":"mYsQluXvLIgbzzNZAAAM"}}
[B] broadcast {"seq":3,"acceptedAtMs":1793320489044,"mutation":{"tool":1,"type":4,"parent":"pen-f1","x":280,"y":260,"clientMutationId":"f3","socket":"mYsQluXvLIgbzzNZAAAM"}}
要点:服务端为每条接受的操作分配递增 seq 与 acceptedAtMs,并附加来源 socket id;同一板的全部连接(含发起者)收到同一条广播——客户端按 clientMutationId 去重自己的乐观操作。落盘验证(约 2 秒后,WBO_SAVE_INTERVAL 默认值):
<svg id="canvas" xmlns="http://www.w3.org/2000/svg" version="1.1" width="5000" height="5000" data-wbo-format="whitebophir-svg-v2" data-wbo-seq="4" data-wbo-readonly="false"><defs id="defs"></defs><g id="drawingArea"><path id="pen-f1" d="M 120 130 l 160 130 l 140 -80" stroke="#0044cc" stroke-width="12" fill="none" stroke-linecap="round" stroke-linejoin="round"></path></g><g id="cursors"></g></svg>
data-wbo-seq="4" 与最后一条广播 seq 一致,证明持久化位点就是全局序号。文本与橡皮同法验证:text CREATE 后用 UPDATE 写 txt(回显 {"seq":2,...,"mutation":{"tool":5,"type":2,"id":"txt-e3","txt":"WBO 2.18.31 evidence",...}});eraser DELETE 对不存在 id 也接受(幂等)。
5.4 拒绝路径实测(mutation_rejected)
错误写法与服务端拒因实测:
| 实测写法 | 服务端回显 | 根因 |
|---|---|---|
{"tool":"path",...}(字符串工具名) | {"reason":"invalid tool","clientMutationId":"mut-e1"} | 协议要求 tool 为数字编码 |
画笔 size:8 | {"reason":"size: invalid size","clientMutationId":"m1"} | 线宽下限 10 |
APPEND 指向不存在的 parent | {"reason":"invalid parent for child","clientMutationId":"m2"} | 必须先 CREATE 父元素 |
集成建议:把 mutation_rejected 当作协议异常处理(打印拒因 + 停止重试),把 rate-limited 当作流控信号(退避后重试)。
5.5 重连与基线回放实测(baseline_not_replayable)
2.18.31 的重连同步机制:客户端重连时在查询参数携带自己的基线序号 baselineSeq,服务端比对内存操作日志(保留窗口 WBO_SEQ_REPLAY_RETENTION_MS,默认 60 秒)后决定「回放增量」还是「拒绝连接」。
实测一:声明超前基线(baselineSeq=99999):
[C-future] connect_error {"data":{"reason":"baseline_not_replayable","latestSeq":0,"minReplayableSeq":0}}
实测二:声明负数基线(baselineSeq=-5):服务端按 0 处理,正常连入并收到 boardstate 与空回放批次,不做基线校验拒绝。
结论:reason=baseline_not_replayable 是重连被拒的统一信号(latestSeq/minReplayableSeq 附带服务端当前窗口),覆盖「基线超前」与「基线早于可回放窗口」两种情况;服务端日志对应 socket.connection_replay_rejected 事件。客户端处理方式:收到该错误后丢弃本地基线、以序号 0 重连全量拉取。服务端治理方式:调大 WBO_SEQ_REPLAY_RETENTION_MS 并保证网关 WS 空闲超时大于它(见 C1 5.2),避免长闲置用户批量触发全量拉取。
六、注意事项
- 写操作只走 WebSocket:HTTP 层没有写接口,自动化集成必须用 socket.io-client(或实现 engine.io 协议),
tool/type用数字编码; - 先 CREATE 后 APPEND:画笔是「建路径 + 逐点追加」两段式,直接 APPEND 会被拒(
invalid parent for child); - 能力位驱动:以
boardstate回包决定可做操作,只读板(readonly:true或canWrite:false)写操作会被拒; clientMutationId全程携带:去重、拒因定位、乐观回滚都依赖它;- 限流是多层闸门:socket 级
WBO_MAX_EMIT_COUNT之外还有按 IP 的建设性/破坏性/文本三档,压测或批量导入时先核对 C1 的档位,避免 429 式静默丢操作(客户端侧表现为rate-limited事件)。
七、常见问题
1. 连接直接 connect_error 带 baseline_not_replayable:重连基线超出服务端窗口,客户端清基线以 0 重连全量拉取;根治靠调大 WBO_SEQ_REPLAY_RETENTION_MS 并联动网关超时(C1 5.2、5.5 实测)。
2. 消息发出无任何反应:先查 mutation_rejected 拒因(§5.4 三种实测),再确认 tool/type 是数字、必填字段齐全、parent 元素已存在。
3. 一切正常但重启服务后板面丢了:WBO_HISTORY_DIR 没有落到持久卷,或重启发生在 WBO_MAX_SAVE_DELAY(默认 60 秒)落盘窗口内;落盘位点以文件头 data-wbo-seq 为准。
4. 通过 nginx 反代后连不上 WebSocket:反代缺 proxy_http_version 1.1 + Upgrade/Connection 升级头(B2 第三节),报错形态是连接阶段 connect_error 或超时。
5. 高频光标同步被限:光标移动也消耗 WBO_MAX_EMIT_COUNT 档,多人大板场景适当上调该档或在客户端做光标节流。
八、参考链接
- 组件画像与同步架构:《WBO 2.18.31 介绍与选型指南》
- 部署与反代配置:《WBO 2.18.31 Docker Compose 部署指南》
- 全量参数与限流档语法:《WBO 2.18.31 配置参数详解》
- Socket.IO 客户端文档:https://socket.io/docs/v4/client-api/
- 源码仓库(协议契约权威来源
client-data/tools/manifest.js、server/socket/):https://github.com/lovasoa/whitebophir