WBO 2.18.31 常用操作与 API 详解

WBO 2.18.31 常用操作与 API 详解:HTTP 端点与 Socket.IO 协议实测手册,覆盖消息契约(MutationType/工具编码/字段范围)、双端同步回显、mutation_rejected 拒因与 baseline_not_replayable 重连实测。

WBOWBOWebSocketAPI白板2026/10/30

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 的对外接口分两层:

层端点/事件用途
HTTPGET /首页(随机板入口)
HTTPGET /boards/<板名>板页,访问即隐式建板
HTTP静态资源(/index.css 等)前端资源
WebSocketsocket.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。

三、前置条件

  1. 一套已运行的 WBO 实例(部署见 B2);
  2. 测试机安装 Node.js ≥ 22.23.4,并安装客户端:
npm install socket.io-client@4.8.4
added 9 packages in 4s
  1. 如需观察服务端行为,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 为空数组,随后增量操作逐条推送。

服务端事件速查:

事件方向说明
boardstateS→C连接能力清单
user_joined / user_leftS→C成员进出
broadcast双向C→S 上行写操作;S→C 同步/回放
mutation_rejectedS→C写操作被校验拒绝(含 reason 与 clientMutationId)
rate-limitedS→C触发限流
active_bansS→C当前封禁名单
moderation_disconnectS→C被版主强制断开

5.2 写操作消息契约

上行消息挂在 broadcast 事件上,type(MutationType)与 tool(工具编码)均为数字:

MutationType 编码表(实测源码 mutation_type.js):

值类型用途
1CREATE新建元素
2UPDATE更新字段(如文本内容)
3DELETE删除元素
4APPEND追加子项(画笔加拐点)
5BATCH批量信封(服务端回放批次)
6CLEAR清板
7COPY复制

工具编码表(实测源码 tools/manifest.js):

值工具值工具
1pencil(画笔)7hand(视角)
2straight-line8grid(网格)
3rectangle9download(导出)
4ellipse10zoom
5text(文本)11clear(清板)
6eraser(橡皮)12cursor(光标)

各工具的字段契约(liveMessageFields 实测原文):

工具type必填字段
pencil(1)1 CREATEid、color、size、opacity?
pencil(1)4 APPENDparent(指向路径 id)、x、y
text(5)1 CREATEid、color、size、opacity?、x、y
text(5)2 UPDATEid、txt
eraser(6)3 DELETEid
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),避免长闲置用户批量触发全量拉取。

六、注意事项

  1. 写操作只走 WebSocket:HTTP 层没有写接口,自动化集成必须用 socket.io-client(或实现 engine.io 协议),tool/type 用数字编码;
  2. 先 CREATE 后 APPEND:画笔是「建路径 + 逐点追加」两段式,直接 APPEND 会被拒(invalid parent for child);
  3. 能力位驱动:以 boardstate 回包决定可做操作,只读板(readonly:true 或 canWrite:false)写操作会被拒;
  4. clientMutationId 全程携带:去重、拒因定位、乐观回滚都依赖它;
  5. 限流是多层闸门: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 档,多人大板场景适当上调该档或在客户端做光标节流。

八、参考链接