OpenClaw远程管理终极篇:WebSocket协议+150个RPC一次讲透,全栈实战!
王炸技术篇来了!
OpenClaw的CLI是一次性的刀——建连、调用、断开,干完就走。Admin HTTP RPC更简单,一个POST请求搞定。但有些场景只靠这两个不够:实时事件推送、双向通信、前端Web页面管理——这些只有WebSocket能做到。
WebSocket JSON-RPC是OpenClaw的唯一控制面传输协议。所有客户端(CLI、Web UI、macOS/iOS/Android App、Node Host)的底层全部走的WebSocket。CLI只是把它封装成了一次性命令,Admin HTTP RPC只是把其中约50个方法暴露成了HTTP接口。你直接对接WebSocket,就是在用OpenClaw最底层、最完整的能力。
这篇把WebSocket JSON-RPC的完整协议拆解出来:握手流程、帧格式、认证体系、150+个RPC方法清单、25+个实时事件、四种语言SDK封装、以及9个实机验证的踩坑记录。读完这篇加上前两篇,你可以用任何语言写一个完整的OpenClaw管理前端。依旧可以丢给AI,直接让基于本文开发一个基于Openclaw的完整的前后端服务,这也是最近笔者在做企业级的claw,总结整理出来的技术沉淀!
OpenClaw Gateway采用WebSocket JSON-RPC作为唯一的控制面传输协议。
协议特点:
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
req(请求)、res(响应)、event(事件推送) |
|
|
|
|
|
|
|
|
|
跟前两篇的对比:
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

整个握手流程就四步:建连 -> challenge -> connect -> hello-ok。
收到 connect.challenge 事件后,客户端必须发送的第一个请求帧:
关键检查: 握手成功后务必检查 payload.auth.scopes 是否包含所需权限。如果为空 [],说明权限被网关安全策略剥离。后面踩坑实录会详细讲这个最阴险的坑。
|
|
|
|
|
|---|---|---|---|
| minProtocol |
|
|
|
| maxProtocol |
|
|
|
| client.id |
|
|
|
| client.version |
|
|
|
| client.platform |
|
|
|
| client.mode |
|
|
|
| client.displayName |
|
|
|
| client.deviceFamily |
|
|
|
| client.instanceId |
|
|
|
| role |
|
|
|
| scopes |
|
|
|
| auth.token |
|
|
|
| auth.password |
|
|
|
| auth.deviceToken |
|
|
|
| auth.bootstrapToken |
|
|
|
| device |
|
|
|
| caps |
|
|
|
| locale |
|
|
|
|
|
|
|---|---|
| openclaw-control-ui |
|
| openclaw-tui |
|
| cli |
|
| gateway-client |
|
| webchat |
|
| webchat-ui |
|
| openclaw-macos |
|
| openclaw-ios |
|
| openclaw-android |
|
| node-host |
|
| openclaw-probe |
|
|
|
|
|---|---|
| ui |
|
| cli |
|
| backend |
|
| webchat |
|
| node |
|
| probe |
|
| test |
|
WebSocket上传输的每一帧都是JSON文本。三种类型:请求、响应、事件。经常做这方面的同学可以跳过看后边~~
成功:
失败:
请求和响应通过 id 字段匹配。事件帧没有 id,是网关主动推送的。一个连接上会同时收到响应帧和事件帧,必须通过 type 区分。
|
|
|
|
|---|---|---|
| operator.admin |
|
|
| operator.write |
|
|
| operator.read |
|
|
| operator.pairing |
|
|
| operator.approvals |
|
|
1. Device Token(已配对设备的持久凭证)
2. Shared Token(配置文件中的共享密钥)
3. Password(密码认证)
4. Bootstrap Token(首次引导配对用)
5. Trusted Proxy(反向代理信任)
网关默认采用Default-Deny策略。无设备身份的客户端会被清除scopes。要获得完整权限,有三种方式:
方式一:携带匹配的Origin头(Control UI模式)
方式二:设备签名认证(最安全)
方式三:网关配置 dangerouslyDisableDeviceAuth: true
这个配置项名字就已经告诉你了——危险。只有在完全私有网络里才用。

150+个方法,按功能分19类。每个方法标注了需要的权限Scope和参数。
|
|
|
|
|
|---|---|---|---|
| health |
|
{ probe?: boolean } |
|
| status |
|
{ includeChannelSummary?: boolean } |
|
| diagnostics.stability |
|
{} |
|
| gateway.identity.get |
|
{} |
|
| gateway.restart.preflight |
|
{} |
|
| gateway.restart.request |
|
{} |
|
| system-presence |
|
{} |
|
| system-event |
|
{ text } |
|
| last-heartbeat |
|
{} |
|
| set-heartbeats |
|
{ enabled: boolean } |
|
| wake |
|
{} |
|
| logs.tail |
|
{ lines?, filter? } |
|
| usage.status |
|
{} |
|
| usage.cost |
|
{} |
|
| commands.list |
|
{} |
|
|
|
|
|
|
|---|---|---|---|
| agents.list |
|
{} |
|
| agents.create |
|
{ name, workspace, model?, emoji?, avatar? } |
|
| agents.update |
|
{ agentId, name?, workspace?, model?, emoji?, avatar? } |
|
| agents.delete |
|
{ agentId, deleteFiles?: boolean } |
|
| agents.files.list |
|
{ agentId } |
|
| agents.files.get |
|
{ agentId, name } |
|
| agents.files.set |
|
{ agentId, name, content } |
|
| agent.identity.get |
|
{} |
|
| agent |
|
{ … } |
|
| agent.wait |
|
{ … } |
|
agents.files.set 允许写入的文件白名单:AGENTS.md、SOUL.md、TOOLS.md、IDENTITY.md、USER.md、HEARTBEAT.md、BOOTSTRAP.md、MEMORY.md。
|
|
|
|
|
|---|---|---|---|
| sessions.list |
|
{} |
|
| sessions.create |
|
{ agentId?, … } |
|
| sessions.send |
|
{ sessionKey, message, … } |
|
| sessions.abort |
|
{ sessionKey } |
|
| sessions.preview |
|
{ sessionKey } |
|
| sessions.describe |
|
{ sessionKey } |
|
| sessions.subscribe |
|
{} |
|
| sessions.unsubscribe |
|
{} |
|
| sessions.messages.subscribe |
|
{ sessionKey } |
|
| sessions.messages.unsubscribe |
|
{ sessionKey } |
|
| sessions.patch |
|
{ sessionKey, … } |
|
| sessions.pluginPatch |
|
{ sessionKey, … } |
|
| sessions.reset |
|
{ sessionKey } |
|
| sessions.delete |
|
{ sessionKey } |
|
| sessions.cleanup |
|
{} |
|
| sessions.compact |
|
{ sessionKey } |
|
| sessions.compaction.list |
|
{ sessionKey } |
|
| sessions.compaction.get |
|
{ sessionKey, id } |
|
| sessions.compaction.branch |
|
{ sessionKey, id } |
|
| sessions.compaction.restore |
|
{ sessionKey, id } |
|
|
|
|
|
|
|---|---|---|---|
| chat.history |
|
{ sessionKey?, limit? } |
|
| chat.send |
|
{ message, sessionKey?, … } |
|
| chat.abort |
|
{ sessionKey? } |
|
| send |
|
{ … } |
|
| message.action |
|
{ … } |
|
|
|
|
|
|
|---|---|---|---|
| channels.status |
|
{ probe?: boolean, channel?, timeoutMs? } |
|
| channels.start |
|
{ channel, accountId? } |
|
| channels.stop |
|
{ channel, accountId? } |
|
| channels.logout |
|
{ channel, accountId? } |
|
|
|
|
|
|
|
|---|---|---|---|---|
| models.list |
|
|
|
|
| models.authStatus |
|
{} |
|
|
| models.authLogout |
|
{} |
|
|
|
|
|
|
|---|---|---|---|
| tools.catalog |
|
{ agentId?, includePlugins?: boolean } |
|
| tools.effective |
|
{ sessionKey, agentId? } |
|
| tools.invoke |
|
{ tool, args, sessionKey? } |
|
|
|
|
|
|
|
|---|---|---|---|---|
| skills.status |
|
{ agentId? } |
|
|
| skills.search |
|
{ query?, limit? } |
|
|
| skills.detail |
|
{ slug } |
|
|
| skills.install |
|
|
|
|
| skills.update |
|
{ skillKey, enabled?, apiKey?, env? } |
|
|
| skills.bins |
|
{} |
|
|
| skills.upload.begin |
|
{ … } |
|
|
| skills.upload.chunk |
|
{ … } |
|
|
| skills.upload.commit |
|
{ … } |
|
|
|
|
|
|
|---|---|---|---|
| config.get |
|
{} |
|
| config.set |
|
{ raw, baseHash } |
|
| config.patch |
|
{ raw, baseHash } |
|
| config.apply |
|
{ raw, baseHash } |
|
| config.schema |
|
{} |
|
| config.schema.lookup |
|
{ path } |
|
Config修改流程:
|
|
|
|
|
|---|---|---|---|
| cron.list |
|
{} |
|
| cron.get |
|
{ id } |
|
| cron.status |
|
{} |
|
| cron.add |
|
{ schedule, command, … } |
|
| cron.update |
|
{ id, … } |
|
| cron.remove |
|
{ id } |
|
| cron.run |
|
{ id } |
|
| cron.runs |
|
{ id?, limit? } |
|
|
|
|
|
|
|---|---|---|---|
| tasks.list |
|
{ status?, limit? } |
|
| tasks.get |
|
{ taskId } |
|
| tasks.cancel |
|
{ taskId } |
|
|
|
|
|
|
|---|---|---|---|
| node.list |
|
{} |
|
| node.describe |
|
{ nodeId } |
|
| node.rename |
|
{ nodeId, name } |
|
| node.invoke |
|
{ nodeId, command, … } |
|
| node.pending.enqueue |
|
{ … } |
|
| node.pending.drain |
|
{} |
|
| node.pending.pull |
|
{} |
|
| node.pending.ack |
|
{ … } |
|
| node.invoke.result |
|
{ … } |
|
| node.event |
|
{ … } |
|
| node.pair.request |
|
{ … } |
|
| node.pair.list |
|
{} |
|
| node.pair.approve |
|
{ requestId } |
|
| node.pair.reject |
|
{ requestId } |
|
| node.pair.remove |
|
{ nodeId } |
|
| node.pair.verify |
|
{ … } |
|
| device.pair.list |
|
{} |
|
| device.pair.approve |
|
{ requestId } |
|
| device.pair.reject |
|
{ requestId } |
|
| device.pair.remove |
|
{ deviceId } |
|
| device.token.rotate |
|
{ deviceId } |
|
| device.token.revoke |
|
{ deviceId } |
|
|
|
|
|
|
|---|---|---|---|
| tts.status |
|
{} |
|
| tts.providers |
|
{} |
|
| tts.personas |
|
{} |
|
| tts.enable |
|
{} |
|
| tts.disable |
|
{} |
|
| tts.convert |
|
{ text, … } |
|
| tts.setProvider |
|
{ provider } |
|
| tts.setPersona |
|
{ persona } |
|
| talk.config |
|
{} |
|
| talk.speak |
|
{ … } |
|
| talk.mode |
|
{ … } |
|
| talk.realtime.session |
|
{ … } |
|
| talk.realtime.relayAudio |
|
{ … } |
|
| talk.realtime.relayMark |
|
{ … } |
|
| talk.realtime.relayStop |
|
{ … } |
|
| talk.realtime.relayToolResult |
|
{ … } |
|
|
|
|
|
|
|---|---|---|---|
| voicewake.get |
|
{} |
|
| voicewake.set |
|
{ … } |
|
| voicewake.routing.get |
|
{} |
|
| voicewake.routing.set |
|
{ … } |
|
|
|
|
|
|
|---|---|---|---|
| environments.list |
|
{} |
|
| environments.status |
|
{ id? } |
|
| artifacts.list |
|
{ sessionKey? } |
|
| artifacts.get |
|
{ id } |
|
| artifacts.download |
|
{ id } |
|
|
|
|
|
|
|---|---|---|---|
| plugins.uiDescriptors |
|
{} |
|
| plugins.sessionAction |
|
{ … } |
|
| wizard.start |
|
{ … } |
|
| wizard.next |
|
{ … } |
|
| wizard.cancel |
|
{} |
|
| wizard.status |
|
{} |
|
|
|
|
|
|
|---|---|---|---|
| exec.approvals.get |
|
{} |
|
| exec.approvals.set |
|
{ … } |
|
| exec.approvals.node.get |
|
{ … } |
|
| exec.approvals.node.set |
|
{ … } |
|
| exec.approval.get |
|
{ id } |
|
| exec.approval.request |
|
{ … } |
|
| exec.approval.resolve |
|
{ id, decision } |
|
| plugin.approval.request |
|
{ … } |
|
| plugin.approval.resolve |
|
{ id, decision } |
|
|
|
|
|
|
|---|---|---|---|
| secrets.reload |
|
{} |
|
| secrets.resolve |
|
{ refs } |
|
| update.status |
|
{} |
|
| update.run |
|
{} |
|
|
|
|
|
|
|---|---|---|---|
| doctor.memory.status |
|
{} |
|
| doctor.memory.dreamDiary |
|
{} |
|
| doctor.memory.backfillDreamDiary |
|
{} |
|
| doctor.memory.resetDreamDiary |
|
{} |
|
| doctor.memory.resetGroundedShortTerm |
|
{} |
|
| doctor.memory.repairDreamingArtifacts |
|
{} |
|
| doctor.memory.dedupeDreamDiary |
|
{} |
|
| doctor.memory.remHarness |
|
{} |
|
这是WebSocket相对CLI和HTTP RPC最大的优势——握手完成后,网关会主动推送事件,你不需要轮询。
|
|
|
|
|---|---|---|
| health |
|
{ ok, ts, channels, eventLoop } |
| tick |
|
{ ts } |
| presence |
|
[{ host, ip, mode, … }] |
| chat |
|
{ sessionKey, message, … } |
| agent |
|
{ … } |
| session.message |
|
{ sessionKey, … } |
| session.tool |
|
{ sessionKey, tool, … } |
| sessions.changed |
|
{ … } |
| shutdown |
|
{ reason, restartExpectedMs? } |
| heartbeat |
|
{ agentId, … } |
| cron |
|
{ jobId, … } |
| node.pair.requested |
|
{ requestId, deviceId } |
| node.pair.resolved |
|
{ requestId, approved } |
| node.invoke.request |
|
{ … } |
| device.pair.requested |
|
{ requestId, deviceId } |
| device.pair.resolved |
|
{ requestId, approved } |
| voicewake.changed |
|
{ … } |
| voicewake.routing.changed |
|
{ … } |
| exec.approval.requested |
|
{ id, command, … } |
| exec.approval.resolved |
|
{ id, decision } |
| plugin.approval.requested |
|
{ … } |
| plugin.approval.resolved |
|
{ … } |
| talk.mode |
|
{ … } |
| update.available |
|
{ version } |
四种语言的SDK封装示例,覆盖前端Web页面、后端Node.js、Python集成、配置修改完整流程。
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
agents.files.set
|
|
|
|
|---|---|
|
|
|
|
|
|
|
|
channels.status
|
|
|
|
|
|
|
|
|
|
|
|---|---|---|
| INVALID_REQUEST |
|
|
| UNAVAILABLE |
|
|
| NOT_PAIRED |
|
|
| STARTUP_UNAVAILABLE |
|
|
下面这段代码演示了如何在一个WebSocket连接上并发拉取系统状态、模型、渠道、工具、技能、Agent文件、定时任务、会话列表等所有管理信息:
9个实机验证踩坑记录,全部来自真实环境复现。WebSocket协议比CLI和HTTP复杂得多,坑也最多。
实测现象:
|
|
|
|
|---|---|---|
| openclaw-control-ui |
|
origin not allowed
|
| openclaw-control-ui | https://其他域名 | origin not allowed |
| openclaw-control-ui | https://网关地址
|
|
| cli |
|
|
使用 openclaw-control-ui 客户端ID时,必须在WebSocket连接建立时携带与网关地址匹配的Origin头。否则直接被拒绝,错误信息是 “origin not allowed”:
实测现象:
|
|
|
|
|
|---|---|---|---|
| cli
|
|
|
[]
|
| cli
|
|
|
[“operator.read”,”write”,”admin”] |
| openclaw-control-ui
|
|
|
[“operator.read”,”write”,”admin”] |
后果:握手 hello-ok 返回 ok: true,看起来连接成功了,但 auth.scopes = []。后续调用任何需要权限的方法都报:
连接”成功”不代表有权限。务必检查hello-ok返回的 payload.auth.scopes 是否非空。如果为空,要么换Control UI模式 + Origin头,要么完成设备签名配对。
实测:
返回:
合法的mode值:ui, cli, backend, webchat, node, probe, test。mode 不是填角色名(operator/admin),而是客户端运行模式。
实测:
minProtocol 不要写得太高。建议固定用 minProtocol: 3, maxProtocol: 4 以兼容老版本网关。这是WebSocket脚本方式相对CLI的一个优势——CLI的协议版本是源码硬编码的,你只能升级或降级整个CLI;WebSocket脚本你可以自己控制minProtocol。
握手阶段,收到 connect.challenge 后发送的第一个请求必须是 method: “connect”。发送任何其他方法会被直接断开:
不能在握手完成前发送 health 或其他请求。必须等 hello-ok 返回后才能自由调用。
握手成功后,你会同时收到两种帧:
常见事件干扰:
必须通过 msg.type === ‘res’ && msg.id === yourId 来匹配响应,不能假设收到的下一帧就是你请求的响应。
网关每隔 tickIntervalMs(默认30秒)发送一个 tick 事件。如果长时间没有收到tick,说明连接可能已经断开。
但反过来:如果你的客户端长时间不发送任何数据,某些代理/负载均衡器可能会切断空闲连接。
生产环境建议实现WebSocket ping/pong或在空闲时定期发一个轻量请求(如 health)保活。
如果提供了device签名但该设备从未被配对过:
连接被close 1008断开。此时需要在服务器端执行 openclaw device approve <requestId> 批准该设备。如果不想配对,用Control UI模式 + Origin头代替。
把前两篇和这篇的所有坑点汇总到一张表里:
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
WebSocket的坑最多——但它能力也最完整。
不同的RPC方法对网关版本有要求,这是实际开发中容易踩的坑:
|
|
|
|
|---|---|---|
|
|
|
|
| sessions.usage
|
|
|
| sessions.usage.timeseries
|
|
|
| skills.upload.begin
|
|
|
| skills.install
|
|
|
|
|
|
|
如果你对接的是较老的网关版本,避免使用上面标注的高版本方法,或者降级网关版本。
config.patch 是WebSocket最常用的写操作之一。规则很多,新手容易踩坑。
|
|
|
|---|---|
|
|
{“tools”: {“profile”: “coding”}} |
|
|
{“tools”: {“media”: {“image”: {“enabled”: false}}}} |
|
|
{“tools”: {“media”: {“video”: {“enabled”: false}}}} |
|
|
{“tools”: {“web”: {“search”: {“enabled”: false}}}} |
|
|
{“skills”: {“entries”: {“<skillId>”: {“enabled”: true/false}}}} |
|
|
{“plugins”: {“entries”: {“<pluginId>”: {“enabled”: true/false}}}} |
|
|
{“channels”: {“<channelId>”: {“<configPatch>”}}} |
|
|
{“cron”: {“enabled”: true/false}} |
|
|
{“agents”: {“defaults”: {“model”: {“primary”: “provider/modelId”}}}} |
|
|
{“agents”: {“list”: [{“id”: “main”, “model”: {“primary”: “provider/modelId”}}]}} |
patch的根节点是OpenClaw配置文件的顶层key(tools / skills / plugins / channels / agents / models),不是 “config.tools”。
config.patch 使用JSON Merge Patch (RFC 7386):
往 models.providers.<name>.models[] 追加新模型时,必须把原有模型也带上:
这个坑非常常见,初次接触Merge Patch的人都会撞一次。
一次 config.patch 同时做三件事:
遗漏任何一步的后果:
所有config写操作(config.set / config.patch / config.apply)必须携带 baseHash:
如果两个客户端同时修改,后提交的会报 “config changed since last load; re-run config.get and retry”。需要重新 config.get 拿新hash再重试。
性能优化:缓存baseHash(30秒TTL),首次直接patch,hash失效再config.get重试。
如果你想做一个支持上传Skill压缩包的UI,需要走分片上传协议。
|
|
|
|
|---|---|---|
|
|
|
|
|
|
skills.install.allowUploadedArchives: true | UNAVAILABLE: Uploaded skill archive installs are disabled |
|
|
operator.admin
|
missing scope: operator.admin |
|
|
|
|
|
|
|
|
|
|
|
|
WebSocket可以远程创建子Agent,这是HTTP RPC不支持的能力之一。
|
|
|
|
|---|---|---|
| agents.create | { name, workspace, model?, emoji?, avatar? } |
|
| agents.update | { agentId, name?, workspace?, model?, emoji?, avatar? } |
|
| agents.delete | { agentId, deleteFiles?: true } |
|
WebSocket最大的性能优势:同一个连接上同时发N个请求,通过ID匹配响应:
CLI每次调用都是一次性的WebSocket握手+连接+断开,这个开销在多请求场景下会被放大很多倍。
避免每次写操作都串行 GetConfig → PatchConfig(两次RPC):
页面加载时不要逐个HTTP请求拉数据,在后端用一个入口并发调多个RPC,前端一次HTTP拿到所有数据。这是为什么OpenClaw自己的Control UI首屏加载非常快——它就是这么干的。
下面是从源码扒出来的全部RPC方法分类索引,共150+个,覆盖19个功能域。这份索引相当于OpenClaw控制面的完整字典——开发管理前端、写自动化脚本、调试问题时,都可以直接当查询手册用。配合前两篇(curl接管和CLI接管)一起看,OpenClaw的远程管理能力你就全掌握了。
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
握手成功后会推送的事件共25个,按触发频率分类:
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
到这里,OpenClaw远程管理的三种方式就讲完了:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
三种方式不是替代关系,是分工关系:
把三篇都看完,OpenClaw的远程管理能力你就吃透了。下一步就是动手写代码——用任何你喜欢的语言,对接最适合的协议,把OpenClaw真正变成你自己的智能体平台。
收藏这三篇,遇到任何远程管理的问题翻一翻基本都能找到答案。
版权声明:本文由AI技术博客原创,转载请注明出处。
夜雨聆风