clawmatrix 0.1.6 → 0.1.11
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/BOOTSTRAP.md +260 -0
- package/README.md +167 -6
- package/package.json +7 -3
- package/src/cli.ts +128 -54
- package/src/cluster-service.ts +12 -1
- package/src/config.ts +17 -0
- package/src/connection.ts +1 -0
- package/src/debug.ts +5 -0
- package/src/handoff.ts +112 -22
- package/src/index.ts +120 -25
- package/src/local-tools.ts +176 -0
- package/src/model-proxy.ts +194 -71
- package/src/peer-manager.ts +32 -5
- package/src/router.ts +24 -3
- package/src/tool-proxy.ts +4 -1
- package/src/types.ts +26 -0
- package/llms.txt +0 -187
package/BOOTSTRAP.md
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# ClawMatrix — OpenClaw 集群插件安装指南
|
|
2
|
+
|
|
3
|
+
> 本文档供 OpenClaw Agent 参考,用于帮助用户安装和配置 ClawMatrix 插件。
|
|
4
|
+
|
|
5
|
+
## 这个插件是什么
|
|
6
|
+
|
|
7
|
+
ClawMatrix 让多个 OpenClaw Gateway 组成去中心化 mesh 网络。安装后,节点之间可以:
|
|
8
|
+
|
|
9
|
+
- **共享模型**:家庭节点借用内网/云端节点的 LLM(如 Claude、DeepSeek)
|
|
10
|
+
- **委托任务**:将复杂任务 handoff 给远程 Agent(如有代码仓库访问权限的内网 Agent)
|
|
11
|
+
- **远程工具调用**:在远程节点执行命令、读写文件,无需委托整个任务
|
|
12
|
+
- **自动发现和故障转移**:gossip 协议自动发现节点,宕机时自动路由到备份
|
|
13
|
+
|
|
14
|
+
## 安装步骤
|
|
15
|
+
|
|
16
|
+
### 第 1 步:安装插件
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
openclaw plugins install clawmatrix
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### 第 2 步:配置
|
|
23
|
+
|
|
24
|
+
编辑 `openclaw.json`,在 `plugins.entries.clawmatrix` 中添加配置。**所有要组网的节点必须使用相同的 `secret`**。
|
|
25
|
+
|
|
26
|
+
用户需要告知你:
|
|
27
|
+
1. **这个节点的角色**:公网服务器?内网办公机?家庭电脑?
|
|
28
|
+
2. **是否需要接受入站连接**(公网节点通常需要,内网/家庭节点不需要)
|
|
29
|
+
3. **要连接的 peer**(通常是公网节点的 WebSocket 地址)
|
|
30
|
+
4. **本节点提供什么**:有哪些 Agent?有哪些可共享的模型?
|
|
31
|
+
5. **是否允许远程工具执行**(toolProxy)
|
|
32
|
+
|
|
33
|
+
### 第 3 步:重启 Gateway
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
openclaw gateway restart
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 第 4 步:验证
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
openclaw clawmatrix status
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 配置模板
|
|
46
|
+
|
|
47
|
+
根据用户的节点角色,选择对应模板并替换占位值。
|
|
48
|
+
|
|
49
|
+
### 公网节点(中继 + 可选 Agent)
|
|
50
|
+
|
|
51
|
+
适用于:有公网 IP 或域名的云服务器。作为 mesh 的中继枢纽,内网和家庭节点都连它。
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"nodeId": "<唯一节点名,如 cloud-01>",
|
|
56
|
+
"secret": "<所有节点共用的密钥,至少 16 个字符>",
|
|
57
|
+
"listen": true,
|
|
58
|
+
"listenPort": 19000,
|
|
59
|
+
"peers": [],
|
|
60
|
+
"agents": [
|
|
61
|
+
{ "id": "<agent名>", "description": "<agent描述>", "tags": ["<标签>"] }
|
|
62
|
+
],
|
|
63
|
+
"models": [],
|
|
64
|
+
"tags": ["cloud"]
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
注意事项:
|
|
69
|
+
- `listenPort` 需要在防火墙/安全组中放行
|
|
70
|
+
- 生产环境建议配置 TLS,让 peer 使用 `wss://` 连接
|
|
71
|
+
- 如果不跑 Agent 可以把 `agents` 设为 `[]`
|
|
72
|
+
|
|
73
|
+
### 内网/办公节点(有模型或代码仓库)
|
|
74
|
+
|
|
75
|
+
适用于:有 GPU、有 API Key、有内网资源的机器。不需要公网 IP,主动连接公网节点。
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"nodeId": "<唯一节点名,如 office-01>",
|
|
80
|
+
"secret": "<同上>",
|
|
81
|
+
"listen": false,
|
|
82
|
+
"peers": [
|
|
83
|
+
{ "nodeId": "<公网节点名>", "url": "wss://<公网节点地址>:19000" }
|
|
84
|
+
],
|
|
85
|
+
"agents": [
|
|
86
|
+
{ "id": "coder", "description": "有代码仓库访问权限,可以读写代码和执行命令", "tags": ["coding"] }
|
|
87
|
+
],
|
|
88
|
+
"models": [
|
|
89
|
+
{ "id": "claude-sonnet", "provider": "anthropic" },
|
|
90
|
+
{ "id": "deepseek-coder", "provider": "ollama" }
|
|
91
|
+
],
|
|
92
|
+
"tags": ["office", "gpu"],
|
|
93
|
+
"toolProxy": {
|
|
94
|
+
"enabled": true,
|
|
95
|
+
"allow": ["exec", "read", "write", "edit", "web_search"],
|
|
96
|
+
"deny": ["browser", "sessions_spawn"]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
注意事项:
|
|
102
|
+
- `models` 里填这台机器上 OpenClaw 已经配置好的模型(必须本地能用才行)
|
|
103
|
+
- **必须开启 OpenClaw 的 chatCompletions HTTP 端点**,否则模型代理无法工作(见下方「前置配置」)
|
|
104
|
+
- `toolProxy.enabled: true` 才会接受远程工具调用
|
|
105
|
+
- `toolProxy.deny` 优先于 `allow`,建议禁用高风险工具
|
|
106
|
+
|
|
107
|
+
### 家庭/轻量节点(借用集群资源)
|
|
108
|
+
|
|
109
|
+
适用于:个人电脑、轻量设备。没有模型和特殊资源,借用集群。
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"nodeId": "<唯一节点名,如 home-01>",
|
|
114
|
+
"secret": "<同上>",
|
|
115
|
+
"listen": false,
|
|
116
|
+
"peers": [
|
|
117
|
+
{ "nodeId": "<公网节点名>", "url": "wss://<公网节点地址>:19000" }
|
|
118
|
+
],
|
|
119
|
+
"agents": [
|
|
120
|
+
{ "id": "assistant", "description": "个人助手", "tags": ["general"] }
|
|
121
|
+
],
|
|
122
|
+
"models": [],
|
|
123
|
+
"proxyModels": [
|
|
124
|
+
{ "id": "claude-sonnet", "nodeId": "office-01" }
|
|
125
|
+
],
|
|
126
|
+
"tags": ["home"]
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
如果要用集群中的模型,还需要修改 agent 的模型配置:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"agents": {
|
|
135
|
+
"defaults": {
|
|
136
|
+
"model": "clawmatrix/<模型ID>"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
注意事项:
|
|
143
|
+
- `proxyModels` 中的 `nodeId` 可选,指定后精确路由到该节点,不指定则自动查找
|
|
144
|
+
- `proxyModels` 中的模型 ID 必须与远程节点 `models` 中声明的 ID 一致
|
|
145
|
+
|
|
146
|
+
## 完整配置字段参考
|
|
147
|
+
|
|
148
|
+
| 字段 | 类型 | 默认值 | 说明 |
|
|
149
|
+
|------|------|--------|------|
|
|
150
|
+
| `nodeId` | string | *必填* | 节点唯一标识 |
|
|
151
|
+
| `secret` | string | *必填* | 集群共享密钥,最少 16 字符 |
|
|
152
|
+
| `listen` | boolean | `false` | 是否接受入站 WebSocket 连接 |
|
|
153
|
+
| `listenHost` | string | `"0.0.0.0"` | WebSocket 监听地址 |
|
|
154
|
+
| `listenPort` | number | `19000` | 入站 WebSocket 端口 |
|
|
155
|
+
| `peers` | array | `[]` | 要连接的 peer:`{ nodeId, url }` |
|
|
156
|
+
| `agents` | array | `[]` | 本节点提供的 Agent:`{ id, description, tags }` |
|
|
157
|
+
| `models` | array | `[]` | 本节点共享给集群的模型:`{ id, provider, description? }` |
|
|
158
|
+
| `proxyModels` | array | `[]` | 从集群消费的远程模型:`{ id, nodeId?, description? }` |
|
|
159
|
+
| `tags` | array | `[]` | 自由标签,用于能力路由 |
|
|
160
|
+
| `proxyPort` | number | `19001` | 本地模型代理 HTTP 端口 |
|
|
161
|
+
| `handoffTimeout` | number | `600000` | Handoff 超时(毫秒,默认 10 分钟) |
|
|
162
|
+
| `toolProxy.enabled` | boolean | `false` | 允许远程工具执行 |
|
|
163
|
+
| `toolProxy.allow` | array | `[]` | 允许的工具名(`[]` 或 `["*"]` = 全部允许) |
|
|
164
|
+
| `toolProxy.deny` | array | `[]` | 禁止的工具名(优先于 allow) |
|
|
165
|
+
| `toolProxy.maxOutputBytes` | number | `1048576` | 单次响应最大字节数 |
|
|
166
|
+
|
|
167
|
+
## 安装后 Agent 获得的工具
|
|
168
|
+
|
|
169
|
+
安装后,本节点的 Agent 自动获得 7 个集群工具:
|
|
170
|
+
|
|
171
|
+
| 工具 | 用途 | 关键参数 |
|
|
172
|
+
|------|------|----------|
|
|
173
|
+
| `cluster_peers` | 查看集群拓扑和连接状态 | 无 |
|
|
174
|
+
| `cluster_handoff` | 委托任务给远程 Agent | `target`, `task`, `context?` |
|
|
175
|
+
| `cluster_send` | 向远程 Agent 发单向消息 | `target`, `message` |
|
|
176
|
+
| `cluster_exec` | 在远程节点执行命令 | `node`, `command`, `workdir?`, `timeout?` |
|
|
177
|
+
| `cluster_read` | 读取远程节点文件 | `node`, `path` |
|
|
178
|
+
| `cluster_write` | 写入远程节点文件 | `node`, `path`, `content` |
|
|
179
|
+
| `cluster_tool` | 调用远程节点任意 OpenClaw 工具 | `node`, `tool`, `args` |
|
|
180
|
+
|
|
181
|
+
`target` 参数支持 Agent ID(如 `"coder"`)或标签查询(如 `"tags:coding"`)。
|
|
182
|
+
`node` 参数支持 nodeId(如 `"office-01"`)或标签查询(如 `"tags:gpu"`)。
|
|
183
|
+
|
|
184
|
+
## 前置配置:开启 chatCompletions 端点
|
|
185
|
+
|
|
186
|
+
**所有共享模型的节点都必须配置此项**。ClawMatrix 的模型代理通过 OpenClaw Gateway 的 `/v1/chat/completions` HTTP 端点转发请求,该端点默认关闭。
|
|
187
|
+
|
|
188
|
+
在 `openclaw.json` 的 `gateway` 字段下添加:
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"gateway": {
|
|
193
|
+
"http": {
|
|
194
|
+
"endpoints": {
|
|
195
|
+
"chatCompletions": { "enabled": true }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
如果不开启,模型代理请求会返回 404 错误。
|
|
203
|
+
|
|
204
|
+
## 前置配置:注册集群模型到 OpenClaw
|
|
205
|
+
|
|
206
|
+
消费远程模型的节点需要在 `models.providers` 中注册,否则 `/models` 命令看不到集群模型。
|
|
207
|
+
|
|
208
|
+
以 nodeId 作为 provider key(推荐),baseUrl 指向本地模型代理端口:
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{
|
|
212
|
+
"models": {
|
|
213
|
+
"providers": {
|
|
214
|
+
"<远程nodeId>": {
|
|
215
|
+
"baseUrl": "http://127.0.0.1:19001",
|
|
216
|
+
"apiKey": "cluster-internal",
|
|
217
|
+
"auth": "api-key",
|
|
218
|
+
"api": "openai-completions",
|
|
219
|
+
"models": [
|
|
220
|
+
{
|
|
221
|
+
"id": "<模型ID>",
|
|
222
|
+
"name": "<显示名>",
|
|
223
|
+
"reasoning": false,
|
|
224
|
+
"input": ["text"],
|
|
225
|
+
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
|
|
226
|
+
"contextWindow": 200000,
|
|
227
|
+
"maxTokens": 32000
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
配置后使用 `/model <nodeId>/<模型ID>` 切换。
|
|
237
|
+
|
|
238
|
+
## 常见问题排查
|
|
239
|
+
|
|
240
|
+
**节点连不上**:
|
|
241
|
+
- 检查 `secret` 是否所有节点一致
|
|
242
|
+
- 检查公网节点的 `listenPort` 是否在防火墙中放行
|
|
243
|
+
- 检查 `peers` 中的 URL 格式:`wss://host:port` 或 `ws://host:port`
|
|
244
|
+
|
|
245
|
+
**远程工具调用失败**:
|
|
246
|
+
- 确认目标节点的 `toolProxy.enabled` 为 `true`
|
|
247
|
+
- 确认目标工具不在 `toolProxy.deny` 列表中
|
|
248
|
+
- 如果 `toolProxy.allow` 非空,确认目标工具在列表中
|
|
249
|
+
|
|
250
|
+
**模型代理不工作**:
|
|
251
|
+
- 确认远程节点的 `models` 中声明了该模型
|
|
252
|
+
- 确认该模型在远程节点上本地可用(OpenClaw 已配置对应 provider)
|
|
253
|
+
- 确认本节点使用 `clawmatrix/<模型ID>` 格式引用模型
|
|
254
|
+
- 如果用了 `proxyModels.nodeId`,确认该节点在线
|
|
255
|
+
|
|
256
|
+
**查看状态**:
|
|
257
|
+
```bash
|
|
258
|
+
openclaw clawmatrix status # 表格形式
|
|
259
|
+
openclaw clawmatrix peers # JSON 格式
|
|
260
|
+
```
|
package/README.md
CHANGED
|
@@ -1,15 +1,176 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ClawMatrix
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
去中心化 mesh 集群插件,让多个 [OpenClaw](https://github.com/nicepkg/openclaw) Gateway 组成 peer-to-peer 网络,跨节点共享 Agent、模型和工具。
|
|
4
|
+
|
|
5
|
+
## 功能
|
|
6
|
+
|
|
7
|
+
**模型代理** — 家庭节点没有 API Key?通过集群使用内网节点的 LLM,就像本地模型一样。
|
|
8
|
+
|
|
9
|
+
**任务 Handoff** — 需要内网资源?将任务委托给有仓库访问权限的远程 Agent,流式返回结果。
|
|
10
|
+
|
|
11
|
+
**工具代理** — 想在远程节点跑个命令或读个文件?直接调用,不用委托整个任务。
|
|
12
|
+
|
|
13
|
+
**自动发现 & 故障转移** — Gossip 协议自动发现节点,宕机时请求自动路由到备份。
|
|
14
|
+
|
|
15
|
+
## 快速开始
|
|
16
|
+
|
|
17
|
+
> **推荐**:将 [BOOTSTRAP.md](BOOTSTRAP.md) 链接发给 OpenClaw Agent,它会引导你完成安装和配置。
|
|
18
|
+
|
|
19
|
+
### 安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
openclaw plugins install clawmatrix
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 配置
|
|
26
|
+
|
|
27
|
+
编辑 `openclaw.json`,添加插件配置。所有节点共享同一 `secret`。
|
|
28
|
+
|
|
29
|
+
**公网节点**(中继枢纽):
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"plugins": {
|
|
34
|
+
"entries": {
|
|
35
|
+
"clawmatrix": {
|
|
36
|
+
"enabled": true,
|
|
37
|
+
"config": {
|
|
38
|
+
"nodeId": "cloud-01",
|
|
39
|
+
"secret": "your-shared-secret-min-16-chars",
|
|
40
|
+
"listen": true,
|
|
41
|
+
"listenPort": 19000,
|
|
42
|
+
"agents": [
|
|
43
|
+
{ "id": "reviewer", "description": "代码审查", "tags": ["review"] }
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**内网节点**(有模型和仓库):
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"nodeId": "office-01",
|
|
57
|
+
"secret": "your-shared-secret-min-16-chars",
|
|
58
|
+
"peers": [{ "nodeId": "cloud-01", "url": "wss://cloud-01.example.com:19000" }],
|
|
59
|
+
"agents": [{ "id": "coder", "description": "代码开发", "tags": ["coding"] }],
|
|
60
|
+
"models": [
|
|
61
|
+
{ "id": "claude-sonnet", "provider": "anthropic" },
|
|
62
|
+
{ "id": "deepseek-coder", "provider": "ollama" }
|
|
63
|
+
],
|
|
64
|
+
"toolProxy": { "enabled": true, "allow": ["exec", "read", "write", "edit"] }
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**家庭节点**(借用集群资源):
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"nodeId": "home-01",
|
|
73
|
+
"secret": "your-shared-secret-min-16-chars",
|
|
74
|
+
"peers": [{ "nodeId": "cloud-01", "url": "wss://cloud-01.example.com:19000" }],
|
|
75
|
+
"agents": [{ "id": "assistant", "description": "个人助手", "tags": ["general"] }],
|
|
76
|
+
"proxyModels": [{ "id": "claude-sonnet", "nodeId": "office-01" }]
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**重要**:共享模型的节点必须开启 chatCompletions 端点:
|
|
81
|
+
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"gateway": {
|
|
85
|
+
"http": {
|
|
86
|
+
"endpoints": {
|
|
87
|
+
"chatCompletions": { "enabled": true }
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
消费远程模型的节点需要在 `models.providers` 中注册(以 nodeId 为 key,baseUrl 指向 `http://127.0.0.1:19001`),详见 [BOOTSTRAP.md](BOOTSTRAP.md)。
|
|
95
|
+
|
|
96
|
+
使用集群模型:`/model <nodeId>/<模型ID>`。
|
|
97
|
+
|
|
98
|
+
### 启动
|
|
4
99
|
|
|
5
100
|
```bash
|
|
6
|
-
|
|
101
|
+
openclaw gateway restart
|
|
102
|
+
openclaw clawmatrix status
|
|
7
103
|
```
|
|
8
104
|
|
|
9
|
-
|
|
105
|
+
## Agent 工具
|
|
106
|
+
|
|
107
|
+
安装后 Agent 自动获得以下工具:
|
|
108
|
+
|
|
109
|
+
| 工具 | 说明 |
|
|
110
|
+
|------|------|
|
|
111
|
+
| `cluster_handoff` | 委托任务给远程 Agent,等待结果 |
|
|
112
|
+
| `cluster_send` | 向远程 Agent 发送单向消息 |
|
|
113
|
+
| `cluster_peers` | 查看集群拓扑和连接状态 |
|
|
114
|
+
| `cluster_exec` | 在远程节点执行命令 |
|
|
115
|
+
| `cluster_read` | 读取远程节点文件 |
|
|
116
|
+
| `cluster_write` | 写入远程节点文件 |
|
|
117
|
+
| `cluster_tool` | 调用远程节点任意 OpenClaw 工具 |
|
|
118
|
+
|
|
119
|
+
目标参数支持 nodeId(`"office-01"`)或标签查询(`"tags:coding"`)。
|
|
120
|
+
|
|
121
|
+
## 配置参考
|
|
122
|
+
|
|
123
|
+
| 字段 | 类型 | 默认值 | 说明 |
|
|
124
|
+
|------|------|--------|------|
|
|
125
|
+
| `nodeId` | string | 必填 | 节点唯一标识 |
|
|
126
|
+
| `secret` | string | 必填 | 集群共享密钥(>= 16 字符) |
|
|
127
|
+
| `listen` | boolean | `false` | 接受入站 WS 连接 |
|
|
128
|
+
| `listenHost` | string | `"0.0.0.0"` | 监听地址 |
|
|
129
|
+
| `listenPort` | number | `19000` | 入站 WS 端口 |
|
|
130
|
+
| `peers` | array | `[]` | 要连接的 peer:`{ nodeId, url }` |
|
|
131
|
+
| `agents` | array | `[]` | 本节点 Agent:`{ id, description, tags }` |
|
|
132
|
+
| `models` | array | `[]` | 共享给集群的模型:`{ id, provider }` |
|
|
133
|
+
| `proxyModels` | array | `[]` | 要消费的远程模型:`{ id, nodeId? }` |
|
|
134
|
+
| `tags` | array | `[]` | 自由标签 |
|
|
135
|
+
| `proxyPort` | number | `19001` | 本地模型代理端口 |
|
|
136
|
+
| `handoffTimeout` | number | `600000` | Handoff 超时(ms) |
|
|
137
|
+
| `toolProxy.enabled` | boolean | `false` | 允许远程工具执行 |
|
|
138
|
+
| `toolProxy.allow` | array | `[]` | 允许的工具(`[]` = 全部) |
|
|
139
|
+
| `toolProxy.deny` | array | `[]` | 禁止的工具(优先) |
|
|
140
|
+
|
|
141
|
+
## 架构
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
┌──────────────────────────┐
|
|
145
|
+
│ 公网 Gateway │
|
|
146
|
+
│ listen: true │
|
|
147
|
+
│ listenPort: 19000 │
|
|
148
|
+
└──┬──────────────────┬────┘
|
|
149
|
+
inbound │ │ inbound
|
|
150
|
+
WS conn │ │ WS conn
|
|
151
|
+
┌──┴────┐ ┌────┴──────┐
|
|
152
|
+
│ 家庭 │ │ 内网办公室 │
|
|
153
|
+
└───────┘ └──────────┘
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- 去中心化 mesh,无 leader,无共识协议
|
|
157
|
+
- HMAC-SHA256 challenge-response 认证
|
|
158
|
+
- 消息中继 + TTL 防环 + ID 去重
|
|
159
|
+
- 心跳检测 + 指数退避重连 + 自动故障转移
|
|
160
|
+
- 生产环境建议 `wss://`(TLS)
|
|
161
|
+
|
|
162
|
+
## 开发
|
|
10
163
|
|
|
11
164
|
```bash
|
|
12
|
-
bun
|
|
165
|
+
bun install # 安装依赖
|
|
166
|
+
bun test # 运行测试
|
|
13
167
|
```
|
|
14
168
|
|
|
15
|
-
|
|
169
|
+
## 文档
|
|
170
|
+
|
|
171
|
+
- [技术规格](docs/SPEC.md) — 完整协议、消息类型、安全设计
|
|
172
|
+
- [安装指南](BOOTSTRAP.md) — AI Agent 辅助安装配置
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawmatrix",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "Decentralized mesh cluster plugin for OpenClaw — inter-gateway communication, model proxy, task handoff, and tool proxy.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"src/",
|
|
19
19
|
"!src/**/*.test.ts",
|
|
20
20
|
"openclaw.plugin.json",
|
|
21
|
-
"
|
|
21
|
+
"BOOTSTRAP.md",
|
|
22
22
|
"README.md"
|
|
23
23
|
],
|
|
24
24
|
"openclaw": {
|
|
@@ -28,7 +28,11 @@
|
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
30
|
"test": "bun test",
|
|
31
|
-
"prepublishOnly": "bun test"
|
|
31
|
+
"prepublishOnly": "bun test",
|
|
32
|
+
"release": "bun scripts/release.ts",
|
|
33
|
+
"release:minor": "bun scripts/release.ts minor",
|
|
34
|
+
"release:major": "bun scripts/release.ts major",
|
|
35
|
+
"release:dry": "bun scripts/release.ts --dry-run"
|
|
32
36
|
},
|
|
33
37
|
"dependencies": {
|
|
34
38
|
"ws": "^8.19.0",
|
package/src/cli.ts
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
-
import {
|
|
2
|
+
import { spawnProcess } from "./compat.ts";
|
|
3
|
+
|
|
4
|
+
async function callGateway(method: string): Promise<unknown> {
|
|
5
|
+
const proc = spawnProcess(["openclaw", "gateway", "call", method, "--json"], {
|
|
6
|
+
stdout: "pipe",
|
|
7
|
+
stderr: "pipe",
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const stdoutChunks: Uint8Array[] = [];
|
|
11
|
+
const stderrChunks: Uint8Array[] = [];
|
|
12
|
+
|
|
13
|
+
const readStream = async (stream: ReadableStream | null, target: Uint8Array[]) => {
|
|
14
|
+
if (!stream) return;
|
|
15
|
+
const reader = stream.getReader();
|
|
16
|
+
while (true) {
|
|
17
|
+
const { done, value } = await reader.read();
|
|
18
|
+
if (done) break;
|
|
19
|
+
target.push(value);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
await Promise.all([
|
|
24
|
+
readStream(proc.stdout, stdoutChunks),
|
|
25
|
+
readStream(proc.stderr, stderrChunks),
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
const code = await proc.exited;
|
|
29
|
+
const stdout = Buffer.concat(stdoutChunks).toString("utf-8").trim();
|
|
30
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf-8").trim();
|
|
31
|
+
|
|
32
|
+
if (code !== 0) {
|
|
33
|
+
// Extract meaningful error from stderr
|
|
34
|
+
const errLine = stderr.split("\n").find((l) => l.includes("Error:") || l.includes("error"));
|
|
35
|
+
throw new Error(errLine || stderr || "Gateway call failed (exit code " + code + ")");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!stdout) {
|
|
39
|
+
throw new Error("Empty response from gateway");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return JSON.parse(stdout);
|
|
43
|
+
}
|
|
3
44
|
|
|
4
45
|
export const registerClusterCli = ({ program }: { program: Command }) => {
|
|
5
46
|
const cmd = program.command("clawmatrix").description("ClawMatrix cluster management");
|
|
@@ -7,78 +48,111 @@ export const registerClusterCli = ({ program }: { program: Command }) => {
|
|
|
7
48
|
cmd
|
|
8
49
|
.command("status")
|
|
9
50
|
.description("Show cluster topology and peer status")
|
|
10
|
-
.action(() => {
|
|
11
|
-
let
|
|
51
|
+
.action(async () => {
|
|
52
|
+
let data: Record<string, unknown>;
|
|
12
53
|
try {
|
|
13
|
-
|
|
54
|
+
data = (await callGateway("clawmatrix.status")) as Record<string, unknown>;
|
|
14
55
|
} catch {
|
|
15
|
-
console.log("
|
|
56
|
+
console.log("Could not reach gateway. Is it running?");
|
|
16
57
|
return;
|
|
17
58
|
}
|
|
18
59
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
// Header
|
|
23
|
-
console.log(`\nNode: ${config.nodeId}`);
|
|
24
|
-
console.log(
|
|
25
|
-
`Listen: ${config.listen ? `port ${config.listenPort}` : "no"}`,
|
|
26
|
-
);
|
|
27
|
-
console.log(`Model proxy: port ${config.proxyPort}`);
|
|
28
|
-
console.log(`Agents: [${config.agents.map((a) => a.id).join(", ")}]`);
|
|
29
|
-
console.log(
|
|
30
|
-
`Models: [${config.models.map((m) => m.id).join(", ")}]`,
|
|
31
|
-
);
|
|
32
|
-
console.log("");
|
|
33
|
-
|
|
34
|
-
if (peers.length === 0) {
|
|
35
|
-
console.log("No peers connected.");
|
|
60
|
+
if (data.error) {
|
|
61
|
+
console.log(String(data.error));
|
|
36
62
|
return;
|
|
37
63
|
}
|
|
38
64
|
|
|
39
|
-
//
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
65
|
+
// Style helpers
|
|
66
|
+
const bold = (s: string) => `\x1b[1m${s}\x1b[22m`;
|
|
67
|
+
const dim = (s: string) => `\x1b[2m${s}\x1b[22m`;
|
|
68
|
+
const green = (s: string) => `\x1b[32m${s}\x1b[39m`;
|
|
69
|
+
const red = (s: string) => `\x1b[31m${s}\x1b[39m`;
|
|
70
|
+
const cyan = (s: string) => `\x1b[36m${s}\x1b[39m`;
|
|
71
|
+
const yellow = (s: string) => `\x1b[33m${s}\x1b[39m`;
|
|
72
|
+
const bar = dim("│");
|
|
73
|
+
const lbl = (text: string) => dim(text.padEnd(13));
|
|
74
|
+
|
|
75
|
+
const agents = data.agents as Array<{ id: string }>;
|
|
76
|
+
const models = data.models as Array<{ id: string }>;
|
|
77
|
+
const tags = data.tags as string[];
|
|
78
|
+
|
|
79
|
+
// Local node
|
|
80
|
+
console.log();
|
|
81
|
+
console.log(` ${cyan("◆")} ${bold("ClawMatrix Cluster")}`);
|
|
82
|
+
console.log(` ${bar}`);
|
|
83
|
+
console.log(` ${bar} ${lbl("Node")}${bold(String(data.nodeId))}`);
|
|
84
|
+
if (tags.length > 0) {
|
|
85
|
+
console.log(` ${bar} ${lbl("Tags")}${tags.join(dim(", "))}`);
|
|
52
86
|
}
|
|
53
|
-
console.log("");
|
|
87
|
+
console.log(` ${bar} ${lbl("Listen")}${data.listen !== false ? `:${data.listen}` : dim("disabled")}`);
|
|
88
|
+
console.log(` ${bar} ${lbl("Model Proxy")}:${data.proxyPort}`);
|
|
89
|
+
console.log(` ${bar} ${lbl("Agents")}${agents.map((a) => a.id).join(dim(", ")) || dim("–")}`);
|
|
90
|
+
console.log(` ${bar} ${lbl("Models")}${models.map((m) => m.id).join(dim(", ")) || dim("–")}`);
|
|
91
|
+
|
|
92
|
+
const peers = data.peers as Array<{
|
|
93
|
+
nodeId: string;
|
|
94
|
+
agents: Array<{ id: string }>;
|
|
95
|
+
models: Array<{ id: string }>;
|
|
96
|
+
tags: string[];
|
|
97
|
+
connected: boolean;
|
|
98
|
+
latencyMs: number;
|
|
99
|
+
}>;
|
|
100
|
+
|
|
101
|
+
if (!peers || peers.length === 0) {
|
|
102
|
+
console.log(` ${bar}`);
|
|
103
|
+
console.log(` ${dim("◇")} ${dim("No peers discovered")}`);
|
|
104
|
+
console.log();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const connected = peers.filter((p) => p.connected).length;
|
|
109
|
+
const countStr = `${connected}/${peers.length} connected`;
|
|
110
|
+
const countColor = connected === peers.length ? green : connected > 0 ? yellow : red;
|
|
111
|
+
|
|
112
|
+
console.log(` ${bar}`);
|
|
113
|
+
console.log(` ${cyan("◆")} ${bold("Peers")} ${countColor(countStr)}`);
|
|
114
|
+
console.log(` ${bar}`);
|
|
115
|
+
|
|
116
|
+
for (let i = 0; i < peers.length; i++) {
|
|
117
|
+
const peer = peers[i];
|
|
118
|
+
const dot = peer.connected ? green("●") : red("○");
|
|
119
|
+
const latency = peer.connected && peer.latencyMs > 0 ? dim(` ${peer.latencyMs}ms`) : "";
|
|
120
|
+
const status = peer.connected ? "" : red(" disconnected");
|
|
121
|
+
console.log(` ${bar} ${dot} ${bold(peer.nodeId)}${status}${latency}`);
|
|
122
|
+
|
|
123
|
+
if (peer.tags.length > 0) {
|
|
124
|
+
console.log(` ${bar} ${lbl("Tags")}${peer.tags.join(dim(", "))}`);
|
|
125
|
+
}
|
|
126
|
+
const peerAgents = peer.agents.map((a) => a.id).join(dim(", "));
|
|
127
|
+
if (peerAgents) {
|
|
128
|
+
console.log(` ${bar} ${lbl("Agents")}${peerAgents}`);
|
|
129
|
+
}
|
|
130
|
+
const peerModels = peer.models.map((m) => m.id).join(dim(", "));
|
|
131
|
+
if (peerModels) {
|
|
132
|
+
console.log(` ${bar} ${lbl("Models")}${peerModels}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (i < peers.length - 1) {
|
|
136
|
+
console.log(` ${bar}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
console.log(` ${bar}`);
|
|
141
|
+
console.log(` ${dim("◇")}`);
|
|
142
|
+
console.log();
|
|
54
143
|
});
|
|
55
144
|
|
|
56
145
|
cmd
|
|
57
146
|
.command("peers")
|
|
58
147
|
.description("List known peers (JSON)")
|
|
59
|
-
.action(() => {
|
|
60
|
-
let
|
|
148
|
+
.action(async () => {
|
|
149
|
+
let peers: unknown;
|
|
61
150
|
try {
|
|
62
|
-
|
|
151
|
+
peers = await callGateway("clawmatrix.peers");
|
|
63
152
|
} catch {
|
|
64
153
|
console.log("[]");
|
|
65
154
|
return;
|
|
66
155
|
}
|
|
67
|
-
|
|
68
|
-
const peers = runtime.peerManager.router.getAllPeers().map((p) => ({
|
|
69
|
-
nodeId: p.nodeId,
|
|
70
|
-
agents: p.agents,
|
|
71
|
-
models: p.models,
|
|
72
|
-
tags: p.tags,
|
|
73
|
-
connected: !!p.connection?.isOpen,
|
|
74
|
-
reachableVia: p.reachableVia,
|
|
75
|
-
latencyMs: p.latencyMs,
|
|
76
|
-
}));
|
|
77
156
|
console.log(JSON.stringify(peers, null, 2));
|
|
78
157
|
});
|
|
79
158
|
};
|
|
80
|
-
|
|
81
|
-
function padRow(...cols: string[]): string {
|
|
82
|
-
const widths = [16, 24, 30, 14, 8];
|
|
83
|
-
return cols.map((c, i) => c.padEnd(widths[i] ?? 12)).join(" ");
|
|
84
|
-
}
|