跳到主要内容

SSE、WebSocket 与 mDNS

20.1 GET /v1/api/systems/stream​

认证遵循 2.3;启用 Bearer 时必须携带请求头,免 token 模式可直接连接。响应头:

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
X-Accel-Buffering: no
event 名首次推送后续周期data
health连接后立即尝试5 sstatus、uptime_seconds、timestamp
hub连接后立即尝试5 s同主机资源查询
robot连接后立即尝试10 s同机器人状态查询

单次采集失败会跳过该次事件,不保证恰好每个周期都有数据。health SSE 直接读取基础健康服务,不附加 /systems/health handler 添加的 provisioning 对象。

event: health
data: {"status":"ok","uptime_seconds":3600,"timestamp":"2026-11-13T08:00:00Z"}

event: robot
data: {"robot_type":"adapter-name","connected":true,"battery":85}

没有 REST 信封。当前不发送 event id,也不实现基于 Last-Event-ID 的历史补发;断开后重新连接得到新快照。

curl -N -sS -H "Authorization: Bearer $ORSUS_TOKEN" "$ORSUS_ORIGIN/v1/api/systems/stream"

启用 Bearer 的设备使用支持自定义头的 SSE 客户端。原生 EventSource 不能设置 Authorization,不能把 Bearer token 放进 ?token= 来期待此接口认证通过;免 token 设备可直接使用原生 EventSource。

20.2 GET /v1/api/terminal/ws​

URL:ws://<IP>:8898/v1/api/terminal/ws?token=<终端令牌>,HTTPS 网关后改用 wss。

校验的是传入终端 handler 的有效 token。普通启动通常来自 [terminal].token,有效值非空则校验 query token,失败为 HTTP 401/1006;普通 prod 启动还会检查终端 token 配置。标准包接管及制造放行兼容路径设置 FactoryTokenBypass=true,路由装配会把有效终端 token 清空,即使磁盘配置原来有 token,也不在此入口校验。它不是 API auth.tokens 中的 Bearer 令牌,也不是 Linux/SSH 密码。终端会话具有实际 shell 权限。

所有应用消息用 WebSocket Binary 帧,首字节定义:

类型首字节方向后续载荷
Input0x00客户端 → 服务端原始输入字节
Resize0x01客户端 → 服务端恰好 4 字节:cols uint16 大端,rows uint16 大端
Output0x02服务端 → 客户端PTY 输出字节
Error0x03服务端 → 客户端UTF-8 错误文本,随后连接可能关闭

Text 帧会被忽略。Resize 总消息长 5 字节,非法尺寸帧导致错误并结束会话。默认空闲超时 10 分钟,Input/Resize 重置计时;仅有终端输出不重置。会话数受 max_sessions 约束,配置示例为 3;达到上限可能在 WebSocket 升级后返回 Error 帧。

UTF-8 正确编码示例(按编码后的字节数分配缓冲区):

function attachTerminal(origin, terminalToken, onOutput, onError) {
const url = new URL('/v1/api/terminal/ws', origin);
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
if (terminalToken) url.searchParams.set('token', terminalToken);
const ws = new WebSocket(url);
ws.binaryType = 'arraybuffer';
const encoder = new TextEncoder();
const outputDecoder = new TextDecoder();

ws.onmessage = ({ data }) => {
const bytes = new Uint8Array(data);
if (!bytes.length) return;
if (bytes[0] === 0x02) {
onOutput(outputDecoder.decode(bytes.subarray(1), { stream: true }));
} else if (bytes[0] === 0x03) {
onError(new TextDecoder().decode(bytes.subarray(1)));
}
};
const send = frame => {
if (ws.readyState !== WebSocket.OPEN) throw new Error('终端尚未连接');
ws.send(frame);
};
return {
ws,
input(text) {
const payload = encoder.encode(text);
const frame = new Uint8Array(payload.length + 1);
frame[0] = 0x00;
frame.set(payload, 1);
send(frame);
},
resize(cols, rows) {
if (![cols, rows].every(n => Number.isInteger(n) && n > 0 && n <= 65535)) {
throw new Error('窗口尺寸必须为 1–65535 的整数');
}
const frame = new ArrayBuffer(5);
const view = new DataView(frame);
view.setUint8(0, 0x01);
view.setUint16(1, cols, false);
view.setUint16(3, rows, false);
send(frame);
},
close() { ws.close(); }
};
}

客户端收到 open 后再发首个 Resize;关闭页面时关闭连接。会话断开不提供 shell 会话续接保证。

20.3 mDNS 发现​

  • 服务类型:_gshub._tcp;域:local.。
  • 实例名:GSHub-<device_id>;TXT 当前为 proto=1。
  • 端口来自正在运行的 HTTP 服务配置,不应硬编码历史端口。
  • 通过活动 IPv4、多播网络接口发布,约每 30 秒刷新以适应接口变化。
  • 发现是尽力而为,多播不通不表示 HTTP 不可用;可直接使用部署提供的 IP。

发现后仍调用 systems/device 校验 SN,再携带正确凭据调用业务接口。