@coclaw/openclaw-coclaw 0.12.2 → 0.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -3
- package/package.json +3 -2
- package/src/auto-upgrade/updater-check.js +1 -1
- package/src/auto-upgrade/updater-spawn.js +1 -0
- package/src/auto-upgrade/worker-verify.js +1 -1
- package/src/auto-upgrade/worker.js +3 -3
- package/src/cli-registrar.js +1 -1
- package/src/realtime-bridge.js +12 -22
- package/src/webrtc/webrtc-peer.js +6 -2
package/README.md
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
# @coclaw/openclaw-coclaw
|
|
2
2
|
|
|
3
|
-
CoClaw 的 OpenClaw 插件(npm: `@coclaw/openclaw-coclaw`,plugin id: `openclaw-coclaw
|
|
3
|
+
CoClaw 的 OpenClaw 插件(npm: `@coclaw/openclaw-coclaw`,plugin id: `openclaw-coclaw`),运行在 OpenClaw gateway 进程中,是 CoClaw 与 OpenClaw 之间的核心连接层。
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
主要模块:
|
|
6
|
+
|
|
7
|
+
- **realtime bridge** — CoClaw server 与 OpenClaw gateway 之间的 WebSocket 实时消息桥接,支持 RPC 转发和事件广播
|
|
8
|
+
- **WebRTC peer** — 与 CoClaw UI 建立 WebRTC DataChannel 直连,提供 RPC 和文件传输两类通道
|
|
9
|
+
- **session manager** — 会话列表/读取能力(`nativeui.sessions.listAll` / `nativeui.sessions.get`)
|
|
10
|
+
- **chat history manager** — 跟踪和管理 chat reset 产生的孤儿 session(`coclaw.chatHistory.list`)
|
|
11
|
+
- **topic manager** — 独立话题的创建、列表、标题生成与删除(`coclaw.topics.*`)
|
|
12
|
+
- **file manager** — 工作区文件管理,支持通过 WebRTC DataChannel 流式传输和 gateway RPC 回退(`coclaw.files.*`)
|
|
7
13
|
- **auto-upgrade** — 从 npm 安装的插件自动检查并升级到最新版本
|
|
14
|
+
- **device identity** — Ed25519 密钥对管理,用于 gateway WebSocket 连接的设备认证
|
|
8
15
|
|
|
9
16
|
## 安装与模式切换
|
|
10
17
|
|
|
@@ -60,6 +67,41 @@ WAIT=1 pnpm run release:check -- 0.1.7 # 轮询直到版本生效
|
|
|
60
67
|
pnpm run release:versions # 显示所有已发布版本
|
|
61
68
|
```
|
|
62
69
|
|
|
70
|
+
## Gateway RPC 方法
|
|
71
|
+
|
|
72
|
+
插件注册的所有 gateway method(通过 `openclaw gateway call <method>` 或 WebRTC DC 调用):
|
|
73
|
+
|
|
74
|
+
| 方法 | 说明 |
|
|
75
|
+
|------|------|
|
|
76
|
+
| `coclaw.bind` | 绑定 Claw 到 CoClaw server |
|
|
77
|
+
| `coclaw.unbind` | 解绑并停止 bridge |
|
|
78
|
+
| `coclaw.enroll` | 生成认领码,等待用户完成绑定 |
|
|
79
|
+
| `coclaw.info` / `coclaw.info.get` | 获取插件版本、claw 版本、capabilities、名称、主机名 |
|
|
80
|
+
| `coclaw.info.patch` | 修改 claw 显示名称,广播 `coclaw.info.updated` 事件 |
|
|
81
|
+
| `coclaw.topics.create` | 创建话题 |
|
|
82
|
+
| `coclaw.topics.list` | 列出指定 agent 的话题 |
|
|
83
|
+
| `coclaw.topics.get` | 获取单个话题 |
|
|
84
|
+
| `coclaw.topics.getHistory` | 获取话题对话记录 |
|
|
85
|
+
| `coclaw.topics.update` | 更新话题标题 |
|
|
86
|
+
| `coclaw.topics.generateTitle` | 通过 agent RPC 自动生成话题标题 |
|
|
87
|
+
| `coclaw.topics.delete` | 删除话题及其 `.jsonl` 文件 |
|
|
88
|
+
| `coclaw.chatHistory.list` | 列出 chat 的历史(孤儿)session |
|
|
89
|
+
| `coclaw.sessions.getById` | 按 sessionId 获取消息记录 |
|
|
90
|
+
| `coclaw.upgradeHealth` | 返回当前插件版本(升级健康检查) |
|
|
91
|
+
| `coclaw.files.list` | 列出工作区文件(RPC 回退) |
|
|
92
|
+
| `coclaw.files.delete` | 删除工作区文件/目录 |
|
|
93
|
+
| `coclaw.files.mkdir` | 创建工作区目录 |
|
|
94
|
+
| `coclaw.files.create` | 创建空文件 |
|
|
95
|
+
| `nativeui.sessions.listAll` | 列出所有 session(分页 + 标题推导) |
|
|
96
|
+
| `nativeui.sessions.get` | 获取 session 原始 JSONL 行(分页) |
|
|
97
|
+
|
|
98
|
+
## Gateway Services
|
|
99
|
+
|
|
100
|
+
| Service ID | 说明 |
|
|
101
|
+
|------------|------|
|
|
102
|
+
| `coclaw-realtime-bridge` | CoClaw server WebSocket 桥接 + WebRTC peer 管理 |
|
|
103
|
+
| `coclaw-auto-upgrade` | npm 安装模式下的自动升级调度器 |
|
|
104
|
+
|
|
63
105
|
## 绑定 / 解绑
|
|
64
106
|
|
|
65
107
|
绑定码从 CoClaw Web 端生成,有效期有限。
|
|
@@ -183,6 +225,19 @@ openclaw logs --limit 300 --plain | rg -n "gateway connect failed|protocol misma
|
|
|
183
225
|
openclaw logs --limit 300 --plain | rg -n "ui->server req|bot->server res|bot->server event|gateway req" -i
|
|
184
226
|
```
|
|
185
227
|
|
|
228
|
+
## 设置
|
|
229
|
+
|
|
230
|
+
插件设置存储在 `~/.openclaw/coclaw/settings.json`,独立于绑定信息,解绑后重新绑定不会丢失。
|
|
231
|
+
|
|
232
|
+
当前支持的设置项:
|
|
233
|
+
- `name` — Claw 显示名称(可选,最长 63 字符),通过 `coclaw.info.patch` RPC 修改
|
|
234
|
+
|
|
235
|
+
`settings.js` 是读写设置的唯一入口。
|
|
236
|
+
|
|
237
|
+
## 设备身份
|
|
238
|
+
|
|
239
|
+
插件在首次运行时自动生成 Ed25519 密钥对,存储在 `~/.openclaw/coclaw/device-identity.json`(mode 0o600)。`deviceId` 为公钥的 SHA-256 摘要,用于 gateway WebSocket 连接的设备认证(v3 auth payload)。
|
|
240
|
+
|
|
186
241
|
## 测试门禁
|
|
187
242
|
|
|
188
243
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coclaw/openclaw-coclaw",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"description": "OpenClaw CoClaw channel plugin for remote chat",
|
|
@@ -60,7 +60,8 @@
|
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"node-datachannel": "0.32.2",
|
|
63
|
-
"werift": "^0.19.0"
|
|
63
|
+
"werift": "^0.19.0",
|
|
64
|
+
"ws": "^8.19.0"
|
|
64
65
|
},
|
|
65
66
|
"devDependencies": {
|
|
66
67
|
"c8": "^10.1.3",
|
|
@@ -37,7 +37,7 @@ export async function getLatestVersion(pkgName, opts) {
|
|
|
37
37
|
return new Promise((resolve, reject) => {
|
|
38
38
|
doExecFile('npm', ['view', pkgName, 'version'], {
|
|
39
39
|
timeout: 30_000,
|
|
40
|
-
shell:
|
|
40
|
+
shell: process.platform === 'win32',
|
|
41
41
|
}, (err, stdout) => {
|
|
42
42
|
if (err) {
|
|
43
43
|
reject(new Error(`npm view failed: ${err.message}`));
|
|
@@ -26,7 +26,7 @@ const CMD_TIMEOUT_MS = 30_000;
|
|
|
26
26
|
function runCmd(cmd, args, opts) {
|
|
27
27
|
const doExecFile = opts?.execFileFn ?? nodeExecFile;
|
|
28
28
|
return new Promise((resolve, reject) => {
|
|
29
|
-
doExecFile(cmd, args, { timeout: CMD_TIMEOUT_MS, shell:
|
|
29
|
+
doExecFile(cmd, args, { timeout: CMD_TIMEOUT_MS, shell: process.platform === 'win32' }, (err, stdout) => {
|
|
30
30
|
if (err) reject(err);
|
|
31
31
|
else resolve(String(stdout).trim());
|
|
32
32
|
});
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* 注意:
|
|
10
10
|
* - 本模块作为独立 node 进程运行,与 gateway 进程隔离
|
|
11
11
|
* - state dir 通过 OPENCLAW_STATE_DIR 环境变量由 spawner 传入
|
|
12
|
-
* - shell
|
|
12
|
+
* - shell 仅在 Windows 启用(openclaw 全局安装生成 .cmd 包装器,需 shell 解析)
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { execFile as nodeExecFile } from 'node:child_process';
|
|
@@ -34,7 +34,7 @@ function runPluginUpdate(pluginId, opts) {
|
|
|
34
34
|
return new Promise((resolve, reject) => {
|
|
35
35
|
doExecFile('openclaw', ['plugins', 'update', pluginId], {
|
|
36
36
|
timeout: 120_000,
|
|
37
|
-
shell:
|
|
37
|
+
shell: process.platform === 'win32',
|
|
38
38
|
}, (err) => {
|
|
39
39
|
if (err) reject(new Error(`plugins update failed: ${err.message}`));
|
|
40
40
|
else resolve();
|
|
@@ -63,7 +63,7 @@ async function fallbackInstallOldVersion(pkgName, version, pluginId, opts) {
|
|
|
63
63
|
}
|
|
64
64
|
const doExecFile = opts?.execFileFn ?? nodeExecFile;
|
|
65
65
|
const run = (args, timeout = 120_000) => new Promise((resolve, reject) => {
|
|
66
|
-
doExecFile('openclaw', args, { timeout, shell:
|
|
66
|
+
doExecFile('openclaw', args, { timeout, shell: process.platform === 'win32' }, (err) => {
|
|
67
67
|
if (err) reject(err);
|
|
68
68
|
else resolve();
|
|
69
69
|
});
|
package/src/cli-registrar.js
CHANGED
|
@@ -87,7 +87,7 @@ function handleRpcError(result, fallbackMsg) {
|
|
|
87
87
|
* @param {object} ctx.logger - 日志实例
|
|
88
88
|
* @param {object} [deps] - 可注入依赖(测试用)
|
|
89
89
|
*/
|
|
90
|
-
export function registerCoclawCli({ program, logger }, deps = {}) {
|
|
90
|
+
export function registerCoclawCli({ program, logger: _logger }, deps = {}) {
|
|
91
91
|
const coclaw = program
|
|
92
92
|
.command('coclaw')
|
|
93
93
|
.description('CoClaw bind/unbind commands');
|
package/src/realtime-bridge.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import nodePath from 'node:path';
|
|
4
|
+
import { WebSocket as WsWebSocket } from 'ws';
|
|
4
5
|
|
|
5
6
|
import { clearConfig, getBindingsPath, readConfig } from './config.js';
|
|
6
7
|
import { getHostName, readSettings } from './settings.js';
|
|
@@ -85,7 +86,7 @@ export class RealtimeBridge {
|
|
|
85
86
|
this.__resolveGatewayAuthToken = deps.resolveGatewayAuthToken ?? defaultResolveGatewayAuthToken;
|
|
86
87
|
this.__loadDeviceIdentity = deps.loadDeviceIdentity ?? loadOrCreateDeviceIdentity;
|
|
87
88
|
this.__preloadNdc = deps.preloadNdc ?? null;
|
|
88
|
-
this.__WebSocket = deps.WebSocket
|
|
89
|
+
this.__WebSocket = deps.WebSocket; // undefined=使用 ws 包, null=禁用(测试用), 其他=自定义实现
|
|
89
90
|
this.__gatewayReadyTimeoutMs = deps.gatewayReadyTimeoutMs ?? 1500;
|
|
90
91
|
|
|
91
92
|
this.serverWs = null;
|
|
@@ -112,7 +113,7 @@ export class RealtimeBridge {
|
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
__resolveWebSocket() {
|
|
115
|
-
return this.__WebSocket
|
|
116
|
+
return this.__WebSocket === undefined ? WsWebSocket : this.__WebSocket;
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
__logDebug(message) {
|
|
@@ -226,7 +227,8 @@ export class RealtimeBridge {
|
|
|
226
227
|
this.webrtcPeer = new WebRtcPeer({
|
|
227
228
|
onSend: (msg) => this.__forwardToServer(msg),
|
|
228
229
|
onRequest: (dcPayload) => {
|
|
229
|
-
|
|
230
|
+
this.__handleGatewayRequestFromDc(dcPayload)
|
|
231
|
+
.catch((err) => this.logger.warn?.(`[coclaw] dc request handler error: ${err?.message}`));
|
|
230
232
|
},
|
|
231
233
|
onFileRpc: (payload, sendFn) => {
|
|
232
234
|
this.__fileHandler.handleRpcRequest(payload, sendFn)
|
|
@@ -625,8 +627,6 @@ export class RealtimeBridge {
|
|
|
625
627
|
return;
|
|
626
628
|
}
|
|
627
629
|
if (payload.type === 'res' || payload.type === 'event') {
|
|
628
|
-
// TODO: UI 已通过 DataChannel 接收业务消息,待旧版 UI 全部更新后移除此转发
|
|
629
|
-
this.__forwardToServer(payload);
|
|
630
630
|
this.webrtcPeer?.broadcast(payload);
|
|
631
631
|
}
|
|
632
632
|
});
|
|
@@ -698,11 +698,11 @@ export class RealtimeBridge {
|
|
|
698
698
|
});
|
|
699
699
|
}
|
|
700
700
|
|
|
701
|
-
async
|
|
701
|
+
async __handleGatewayRequestFromDc(payload) {
|
|
702
702
|
const ready = await this.__waitGatewayReady();
|
|
703
703
|
if (!ready || !this.gatewayWs || this.gatewayWs.readyState !== 1) {
|
|
704
704
|
this.__logDebug(`gateway req drop (offline): id=${payload.id} method=${payload.method}`);
|
|
705
|
-
|
|
705
|
+
this.webrtcPeer?.broadcast({
|
|
706
706
|
type: 'res',
|
|
707
707
|
id: payload.id,
|
|
708
708
|
ok: false,
|
|
@@ -710,9 +710,7 @@ export class RealtimeBridge {
|
|
|
710
710
|
code: 'GATEWAY_OFFLINE',
|
|
711
711
|
message: 'Gateway is offline',
|
|
712
712
|
},
|
|
713
|
-
};
|
|
714
|
-
this.__forwardToServer(errorRes);
|
|
715
|
-
this.webrtcPeer?.broadcast(errorRes);
|
|
713
|
+
});
|
|
716
714
|
return;
|
|
717
715
|
}
|
|
718
716
|
try {
|
|
@@ -725,7 +723,7 @@ export class RealtimeBridge {
|
|
|
725
723
|
}));
|
|
726
724
|
}
|
|
727
725
|
catch {
|
|
728
|
-
|
|
726
|
+
this.webrtcPeer?.broadcast({
|
|
729
727
|
type: 'res',
|
|
730
728
|
id: payload.id,
|
|
731
729
|
ok: false,
|
|
@@ -733,9 +731,7 @@ export class RealtimeBridge {
|
|
|
733
731
|
code: 'GATEWAY_SEND_FAILED',
|
|
734
732
|
message: 'Failed to send request to gateway',
|
|
735
733
|
},
|
|
736
|
-
};
|
|
737
|
-
this.__forwardToServer(errorRes);
|
|
738
|
-
this.webrtcPeer?.broadcast(errorRes);
|
|
734
|
+
});
|
|
739
735
|
}
|
|
740
736
|
}
|
|
741
737
|
|
|
@@ -848,13 +844,6 @@ export class RealtimeBridge {
|
|
|
848
844
|
}
|
|
849
845
|
return;
|
|
850
846
|
}
|
|
851
|
-
if (payload?.type === 'req' || payload?.type === 'rpc.req') {
|
|
852
|
-
void this.__handleGatewayRequestFromServer({
|
|
853
|
-
id: payload.id,
|
|
854
|
-
method: payload.method,
|
|
855
|
-
params: payload.params ?? {},
|
|
856
|
-
});
|
|
857
|
-
}
|
|
858
847
|
}
|
|
859
848
|
catch (err) {
|
|
860
849
|
this.logger.warn?.(`[coclaw] realtime message parse failed: ${String(err?.message ?? err)}`);
|
|
@@ -1029,7 +1018,8 @@ export async function restartRealtimeBridge(opts) {
|
|
|
1029
1018
|
await singleton.stop();
|
|
1030
1019
|
singleton = null;
|
|
1031
1020
|
}
|
|
1032
|
-
|
|
1021
|
+
const deps = opts?.__deps; // 仅测试用
|
|
1022
|
+
singleton = new RealtimeBridge(deps);
|
|
1033
1023
|
await singleton.start(opts);
|
|
1034
1024
|
}
|
|
1035
1025
|
|
|
@@ -236,8 +236,12 @@ export class WebRtcPeer {
|
|
|
236
236
|
this.__logDebug(`ICE candidate from ${connId} but no session`);
|
|
237
237
|
return;
|
|
238
238
|
}
|
|
239
|
-
|
|
240
|
-
|
|
239
|
+
try {
|
|
240
|
+
await session.pc.addIceCandidate(msg.payload);
|
|
241
|
+
this.__logDebug(`[${connId}] ICE candidate added`);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
this.__logDebug(`[${connId}] addIceCandidate failed: ${err?.message}`);
|
|
244
|
+
}
|
|
241
245
|
}
|
|
242
246
|
|
|
243
247
|
__setupDataChannel(connId, dc) {
|