libp2p-mesh 2026.6.18 → 2026.6.20
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 +222 -4
- package/dist/src/agent-tools.d.ts +34 -14
- package/dist/src/agent-tools.js +100 -15
- package/dist/src/inbound.js +2 -1
- package/dist/src/instance-router.js +45 -12
- package/dist/src/labels-wizard.d.ts +21 -0
- package/dist/src/labels-wizard.js +168 -0
- package/dist/src/peer-label-store.d.ts +11 -0
- package/dist/src/peer-label-store.js +172 -0
- package/dist/src/plugin.js +4 -1
- package/dist/src/profile-cli.d.ts +10 -0
- package/dist/src/profile-cli.js +34 -0
- package/dist/src/prompt-cli.d.ts +18 -0
- package/dist/src/prompt-cli.js +48 -0
- package/dist/src/prompt-config.d.ts +11 -0
- package/dist/src/prompt-config.js +188 -0
- package/dist/src/types.d.ts +38 -4
- package/package.json +1 -1
- package/src/agent-tools.ts +145 -22
- package/src/inbound.ts +2 -1
- package/src/instance-router.ts +82 -14
- package/src/labels-wizard.ts +221 -0
- package/src/peer-label-store.ts +224 -0
- package/src/plugin.ts +4 -1
- package/src/profile-cli.ts +49 -0
- package/src/prompt-cli.ts +83 -0
- package/src/prompt-config.ts +207 -0
- package/src/types.ts +43 -2
package/README.md
CHANGED
|
@@ -75,6 +75,159 @@ The generated config shape is:
|
|
|
75
75
|
}
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
+
## 安装后配置流程
|
|
79
|
+
|
|
80
|
+
推荐用户安装插件后按下面顺序配置。所有配置都通过 CLI 完成,不需要手动打开 `openclaw.json`。
|
|
81
|
+
|
|
82
|
+
### 1. 安装或更新插件
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
openclaw plugins update libp2p-mesh@latest
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
如果是首次安装,也可以使用:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
openclaw install libp2p-mesh
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 2. 运行网络和入站目标配置向导
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
openclaw libp2p-mesh setup
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
这个向导会写入:
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
plugins.entries["libp2p-mesh"].config
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
不会写入 `channels["libp2p-mesh"]`。常见选择:
|
|
107
|
+
|
|
108
|
+
- 同一局域网测试:选择 LAN / mDNS。
|
|
109
|
+
- 跨网络或需要 relay:选择 bootstrap / relay 相关模式。
|
|
110
|
+
- 需要把收到的 P2P 消息投递到飞书、QQ、Telegram 等 channel:配置 `inboundTargets`。
|
|
111
|
+
- 暂时只需要工具能力、不接收消息:选择禁用入站投递,写入 `inboundTargets: []`。
|
|
112
|
+
|
|
113
|
+
入站目标示例:
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"id": "feishu-main",
|
|
118
|
+
"channel": "feishu",
|
|
119
|
+
"target": "user:ou_xxx"
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
QQ 单聊示例:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"id": "qqbot-main",
|
|
128
|
+
"channel": "qqbot",
|
|
129
|
+
"target": "user:<senderId>"
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
其中 `<senderId>` 可以从 QQ channel 日志里的 `senderId` 取得。
|
|
134
|
+
|
|
135
|
+
### 3. 一键安装固定 Agent 提示词
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
openclaw libp2p-mesh prompt install
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
该命令会把内置的 P2P 中继助手规则写入:
|
|
142
|
+
|
|
143
|
+
```text
|
|
144
|
+
~/.openclaw/workspace/AGENTS.md
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
它不会覆盖整个文件,只维护下面两个 marker 之间的区块:
|
|
148
|
+
|
|
149
|
+
```md
|
|
150
|
+
<!-- libp2p-mesh:prompt:start -->
|
|
151
|
+
# P2P 中继助手规则
|
|
152
|
+
...
|
|
153
|
+
<!-- libp2p-mesh:prompt:end -->
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
如果区块已经存在,命令会询问是否更新到插件内置的最新版。
|
|
157
|
+
|
|
158
|
+
### 4. 可选:配置用户公开属性
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
openclaw libp2p-mesh profile
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`USER.md` 中的公开 tag 会在 gateway 启动时只读提取,不会写回 `USER.md`。用户手动新增的结构化属性会保存到:
|
|
165
|
+
|
|
166
|
+
```text
|
|
167
|
+
~/.openclaw/libp2p/user-profile.json
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
例如:
|
|
171
|
+
|
|
172
|
+
```json
|
|
173
|
+
{
|
|
174
|
+
"kind": "structured",
|
|
175
|
+
"key": "group",
|
|
176
|
+
"value": "实验室",
|
|
177
|
+
"label": "实验室",
|
|
178
|
+
"source": "profile"
|
|
179
|
+
}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
之后按属性发送时使用 selector:
|
|
183
|
+
|
|
184
|
+
```text
|
|
185
|
+
group=实验室
|
|
186
|
+
tag:P2P
|
|
187
|
+
#P2P
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`selector` 是发送工具调用时的临时匹配条件,不需要写入配置文件。
|
|
191
|
+
|
|
192
|
+
### 5. 可选:配置 announce 日志级别
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
openclaw libp2p-mesh debug
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
推荐保持默认 `summary`。如果排查发现、属性或地址广播问题,可以临时切到 `payload` 查看完整 announce JSON;排查结束后再切回 `summary` 或 `off`。
|
|
199
|
+
|
|
200
|
+
### 6. 可选:配置远端实例本地标签
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
openclaw libp2p-mesh labels
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Labels are private local notes for remote instances you have already discovered. Use them when you want your own grouping to drive sends without asking the remote user to publish that attribute.
|
|
207
|
+
|
|
208
|
+
### 7. 重启 gateway
|
|
209
|
+
|
|
210
|
+
完成 `setup`、`prompt install` 或 `profile` 后,重启 gateway 让配置和提示词生效:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
openclaw gateway restart
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
或者停止后重新运行:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
openclaw gateway
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
启动后可以观察日志:
|
|
223
|
+
|
|
224
|
+
```text
|
|
225
|
+
[libp2p-mesh] Sent instance announce ... attrs=2
|
|
226
|
+
[libp2p-mesh] Received instance announce ... changed=true
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
其中 `attrs` 是本次 announce 中携带的用户公开属性数量。
|
|
230
|
+
|
|
78
231
|
## Configuration
|
|
79
232
|
|
|
80
233
|
Use the interactive setup command for first-time configuration and later edits:
|
|
@@ -471,7 +624,7 @@ Tools are not configured in `openclaw.json`; they are registered automatically b
|
|
|
471
624
|
|
|
472
625
|
`libp2p-mesh` can announce user public attributes with instance route announcements. These attributes help agents find matching OpenClaw instances after those instances have already been discovered through the mesh.
|
|
473
626
|
|
|
474
|
-
There are two sources:
|
|
627
|
+
There are two public sources:
|
|
475
628
|
|
|
476
629
|
- `USER.md` tags are extracted read-only at gateway startup. The plugin never edits `USER.md`.
|
|
477
630
|
- `user-profile.json` stores manually managed structured attributes such as group, project, role, skill, or a custom key.
|
|
@@ -568,27 +721,92 @@ Remote attributes are cached in plugin-managed instance state under `instance-pe
|
|
|
568
721
|
}
|
|
569
722
|
```
|
|
570
723
|
|
|
571
|
-
|
|
724
|
+
### Local peer labels
|
|
725
|
+
|
|
726
|
+
Use local peer labels when you want to classify remote instances privately on this machine:
|
|
727
|
+
|
|
728
|
+
```bash
|
|
729
|
+
openclaw libp2p-mesh labels
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
The default labels path is:
|
|
733
|
+
|
|
734
|
+
```text
|
|
735
|
+
~/.openclaw/libp2p/peer-labels.json
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
When `OPENCLAW_STATE_DIR` is set:
|
|
739
|
+
|
|
740
|
+
```text
|
|
741
|
+
$OPENCLAW_STATE_DIR/libp2p/peer-labels.json
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
Example `peer-labels.json`:
|
|
745
|
+
|
|
746
|
+
```json
|
|
747
|
+
{
|
|
748
|
+
"version": 1,
|
|
749
|
+
"updatedAt": 1782180000000,
|
|
750
|
+
"peers": {
|
|
751
|
+
"alice-mac@AQIDBAUGBweI.7a3f9e2b": {
|
|
752
|
+
"labels": [
|
|
753
|
+
{ "key": "group", "value": "实验室" },
|
|
754
|
+
{ "key": "project", "value": "openclaw" }
|
|
755
|
+
]
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
Privacy boundary: `peer-labels.json` is local state for your gateway. It is not announced to peers, not written into remote `instance-peer.json.userPublicAttributes`, and not visible to the remote user through the mesh protocol. Public attributes in `USER.md` and `user-profile.json` are still broadcast with instance announce messages.
|
|
762
|
+
|
|
763
|
+
Use `p2p_send_user_attribute_message` for attribute-based group messages. It defaults to public attributes only, equivalent to `scope="public"`. Always dry-run first. If the dry run matches targets, call the same tool again immediately with the same selector, scope, message, and `dryRun: false`.
|
|
764
|
+
|
|
765
|
+
Public scope matches attributes that remote instances announced from their own `USER.md` or `user-profile.json`:
|
|
572
766
|
|
|
573
767
|
```text
|
|
574
768
|
p2p_send_user_attribute_message({
|
|
575
769
|
"selector": "project=openclaw",
|
|
576
770
|
"message": "今晚同步一下进展",
|
|
771
|
+
"scope": "public",
|
|
577
772
|
"dryRun": true
|
|
578
773
|
})
|
|
579
774
|
```
|
|
580
775
|
|
|
581
|
-
After
|
|
776
|
+
After a matching dry run:
|
|
582
777
|
|
|
583
778
|
```text
|
|
584
779
|
p2p_send_user_attribute_message({
|
|
585
780
|
"selector": "project=openclaw",
|
|
586
781
|
"message": "今晚同步一下进展",
|
|
782
|
+
"scope": "public",
|
|
587
783
|
"dryRun": false
|
|
588
784
|
})
|
|
589
785
|
```
|
|
590
786
|
|
|
591
|
-
|
|
787
|
+
Local scope, written as `scope="local"` in prompt instructions or `"scope": "local"` in tool JSON, matches only labels from your `peer-labels.json`:
|
|
788
|
+
|
|
789
|
+
```text
|
|
790
|
+
p2p_send_user_attribute_message({
|
|
791
|
+
"selector": "group=实验室",
|
|
792
|
+
"message": "我按本地归类发一个提醒",
|
|
793
|
+
"scope": "local",
|
|
794
|
+
"dryRun": true
|
|
795
|
+
})
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
All scope matches both sources and deduplicates by instance:
|
|
799
|
+
|
|
800
|
+
```text
|
|
801
|
+
p2p_send_user_attribute_message({
|
|
802
|
+
"selector": "project=openclaw",
|
|
803
|
+
"message": "公开属性和本地标签都算",
|
|
804
|
+
"scope": "all",
|
|
805
|
+
"dryRun": true
|
|
806
|
+
})
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
Selectors use `key=value` for structured profile attributes or local labels. Tag matches use `tag:value` or `#value` for public `USER.md` tags. Bare selectors such as `实验室` are rejected because they are ambiguous; use `group=实验室` for a structured group or `tag:实验室` for a USER.md tag.
|
|
592
810
|
|
|
593
811
|
```text
|
|
594
812
|
p2p_send_user_attribute_message({
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type { DeliveryTargetResult, InstanceRouter, MeshNetwork,
|
|
1
|
+
import type { DeliveryTargetResult, InstancePeerRecord, InstanceRouter, LocalPeerLabelAttribute, MeshNetwork, PeerLabelStore } from "./types.js";
|
|
2
|
+
type BuildP2PToolsOptions = {
|
|
3
|
+
peerLabelStore?: Pick<PeerLabelStore, "listLabels">;
|
|
4
|
+
};
|
|
2
5
|
type SendUserAttributeToolParams = {
|
|
3
6
|
selector?: unknown;
|
|
4
7
|
match?: {
|
|
@@ -8,8 +11,9 @@ type SendUserAttributeToolParams = {
|
|
|
8
11
|
};
|
|
9
12
|
message?: unknown;
|
|
10
13
|
dryRun?: unknown;
|
|
14
|
+
scope?: unknown;
|
|
11
15
|
};
|
|
12
|
-
export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter): ({
|
|
16
|
+
export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter, options?: BuildP2PToolsOptions): ({
|
|
13
17
|
name: string;
|
|
14
18
|
label: string;
|
|
15
19
|
description: string;
|
|
@@ -29,8 +33,10 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
29
33
|
selector?: undefined;
|
|
30
34
|
match?: undefined;
|
|
31
35
|
dryRun?: undefined;
|
|
36
|
+
scope?: undefined;
|
|
32
37
|
};
|
|
33
38
|
required: string[];
|
|
39
|
+
anyOf?: undefined;
|
|
34
40
|
};
|
|
35
41
|
execute(_toolCallId: string, params: {
|
|
36
42
|
peerId: string;
|
|
@@ -78,8 +84,10 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
78
84
|
selector?: undefined;
|
|
79
85
|
match?: undefined;
|
|
80
86
|
dryRun?: undefined;
|
|
87
|
+
scope?: undefined;
|
|
81
88
|
};
|
|
82
89
|
required: string[];
|
|
90
|
+
anyOf?: undefined;
|
|
83
91
|
};
|
|
84
92
|
execute(_toolCallId: string, params: {
|
|
85
93
|
topic: string;
|
|
@@ -121,8 +129,10 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
121
129
|
selector?: undefined;
|
|
122
130
|
match?: undefined;
|
|
123
131
|
dryRun?: undefined;
|
|
132
|
+
scope?: undefined;
|
|
124
133
|
};
|
|
125
134
|
required?: undefined;
|
|
135
|
+
anyOf?: undefined;
|
|
126
136
|
};
|
|
127
137
|
execute(_toolCallId: string): Promise<{
|
|
128
138
|
content: {
|
|
@@ -163,8 +173,10 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
163
173
|
selector?: undefined;
|
|
164
174
|
match?: undefined;
|
|
165
175
|
dryRun?: undefined;
|
|
176
|
+
scope?: undefined;
|
|
166
177
|
};
|
|
167
178
|
required?: undefined;
|
|
179
|
+
anyOf?: undefined;
|
|
168
180
|
};
|
|
169
181
|
execute(_toolCallId: string): Promise<{
|
|
170
182
|
content: {
|
|
@@ -214,8 +226,10 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
214
226
|
selector?: undefined;
|
|
215
227
|
match?: undefined;
|
|
216
228
|
dryRun?: undefined;
|
|
229
|
+
scope?: undefined;
|
|
217
230
|
};
|
|
218
231
|
required?: undefined;
|
|
232
|
+
anyOf?: undefined;
|
|
219
233
|
};
|
|
220
234
|
execute(_toolCallId: string): Promise<{
|
|
221
235
|
content: {
|
|
@@ -258,8 +272,10 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
258
272
|
selector?: undefined;
|
|
259
273
|
match?: undefined;
|
|
260
274
|
dryRun?: undefined;
|
|
275
|
+
scope?: undefined;
|
|
261
276
|
};
|
|
262
277
|
required?: undefined;
|
|
278
|
+
anyOf?: undefined;
|
|
263
279
|
};
|
|
264
280
|
execute(_toolCallId: string): Promise<{
|
|
265
281
|
content: {
|
|
@@ -279,18 +295,10 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
279
295
|
text: string;
|
|
280
296
|
}[];
|
|
281
297
|
details: {
|
|
282
|
-
instances: {
|
|
298
|
+
instances: (InstancePeerRecord & {
|
|
283
299
|
connected: boolean;
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
instanceName?: string;
|
|
287
|
-
multiaddrs: string[];
|
|
288
|
-
pubkey?: string;
|
|
289
|
-
userPublicAttributes?: UserPublicAttribute[];
|
|
290
|
-
lastSeenAt: number;
|
|
291
|
-
lastAnnouncedAt: number;
|
|
292
|
-
source: "announce";
|
|
293
|
-
}[];
|
|
300
|
+
localLabels: LocalPeerLabelAttribute[];
|
|
301
|
+
})[];
|
|
294
302
|
count: number;
|
|
295
303
|
initialized?: undefined;
|
|
296
304
|
error?: undefined;
|
|
@@ -326,8 +334,10 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
326
334
|
selector?: undefined;
|
|
327
335
|
match?: undefined;
|
|
328
336
|
dryRun?: undefined;
|
|
337
|
+
scope?: undefined;
|
|
329
338
|
};
|
|
330
339
|
required: string[];
|
|
340
|
+
anyOf?: undefined;
|
|
331
341
|
};
|
|
332
342
|
execute(_toolCallId: string, params: {
|
|
333
343
|
instanceId: string;
|
|
@@ -377,7 +387,7 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
377
387
|
}[];
|
|
378
388
|
details: {
|
|
379
389
|
found: boolean;
|
|
380
|
-
route:
|
|
390
|
+
route: InstancePeerRecord;
|
|
381
391
|
initialized?: undefined;
|
|
382
392
|
error?: undefined;
|
|
383
393
|
instanceId?: undefined;
|
|
@@ -404,8 +414,10 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
404
414
|
selector?: undefined;
|
|
405
415
|
match?: undefined;
|
|
406
416
|
dryRun?: undefined;
|
|
417
|
+
scope?: undefined;
|
|
407
418
|
};
|
|
408
419
|
required: string[];
|
|
420
|
+
anyOf?: undefined;
|
|
409
421
|
};
|
|
410
422
|
execute(_toolCallId: string, params: {
|
|
411
423
|
instanceId: string;
|
|
@@ -521,11 +533,19 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
521
533
|
type: "boolean";
|
|
522
534
|
description: string;
|
|
523
535
|
};
|
|
536
|
+
scope: {
|
|
537
|
+
type: "string";
|
|
538
|
+
enum: string[];
|
|
539
|
+
description: string;
|
|
540
|
+
};
|
|
524
541
|
peerId?: undefined;
|
|
525
542
|
topic?: undefined;
|
|
526
543
|
instanceId?: undefined;
|
|
527
544
|
};
|
|
528
545
|
required: string[];
|
|
546
|
+
anyOf: {
|
|
547
|
+
required: string[];
|
|
548
|
+
}[];
|
|
529
549
|
};
|
|
530
550
|
execute(_toolCallId: string, params: SendUserAttributeToolParams): Promise<{
|
|
531
551
|
content: {
|
package/dist/src/agent-tools.js
CHANGED
|
@@ -23,7 +23,7 @@ function instanceTargetLabel(target) {
|
|
|
23
23
|
return `${target.instanceId}${name} -> ${target.peerId}`;
|
|
24
24
|
}
|
|
25
25
|
function formatUserAttributeTargets(targets) {
|
|
26
|
-
return targets.map((target) => `${instanceTargetLabel(target)} [${attributeLabel(target.matchedAttribute)}]`);
|
|
26
|
+
return targets.map((target) => `${instanceTargetLabel(target)} [${target.matchSource}:${attributeLabel(target.matchedAttribute)}]`);
|
|
27
27
|
}
|
|
28
28
|
function formatUserAttributeResults(results) {
|
|
29
29
|
return results.map((result) => {
|
|
@@ -37,6 +37,78 @@ function formatUserAttributeResults(results) {
|
|
|
37
37
|
return `${instanceTargetLabel(result)}:${status}`;
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
|
+
function formatPublicAttribute(attribute) {
|
|
41
|
+
if (attribute.kind === "tag") {
|
|
42
|
+
return [
|
|
43
|
+
" - kind: tag",
|
|
44
|
+
` value: ${attribute.value}`,
|
|
45
|
+
` label: ${attribute.label}`,
|
|
46
|
+
` source: ${attribute.source}`,
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
return [
|
|
50
|
+
" - kind: structured",
|
|
51
|
+
` key: ${attribute.key}`,
|
|
52
|
+
` value: ${attribute.value}`,
|
|
53
|
+
` label: ${attribute.label}`,
|
|
54
|
+
` source: ${attribute.source}`,
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
function formatLocalLabel(label) {
|
|
58
|
+
return [
|
|
59
|
+
" - kind: structured",
|
|
60
|
+
` key: ${label.key}`,
|
|
61
|
+
` value: ${label.value}`,
|
|
62
|
+
` label: ${label.label}`,
|
|
63
|
+
` source: ${label.source}`,
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
function formatInstanceList(rows) {
|
|
67
|
+
if (rows.length === 0) {
|
|
68
|
+
return "No OpenClaw instances discovered yet.";
|
|
69
|
+
}
|
|
70
|
+
const lines = [`Discovered OpenClaw instances: ${rows.length}`];
|
|
71
|
+
rows.forEach((entry, index) => {
|
|
72
|
+
lines.push("", `${index + 1}. ${entry.instanceId}`, ` peerId: ${entry.peerId}`, ` instanceName: ${entry.instanceName ?? "(none)"}`, ` connected: ${entry.connected}`);
|
|
73
|
+
if ((entry.userPublicAttributes ?? []).length === 0) {
|
|
74
|
+
lines.push(" userPublicAttributes: none");
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
lines.push(" userPublicAttributes:");
|
|
78
|
+
for (const attribute of entry.userPublicAttributes ?? []) {
|
|
79
|
+
lines.push(...formatPublicAttribute(attribute));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (entry.localLabels.length === 0) {
|
|
83
|
+
lines.push(" localLabels: none");
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
lines.push(" localLabels:");
|
|
87
|
+
for (const label of entry.localLabels) {
|
|
88
|
+
lines.push(...formatLocalLabel(label));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
return lines.join("\n");
|
|
93
|
+
}
|
|
94
|
+
function normalizeUserAttributeScope(scope) {
|
|
95
|
+
if (scope === undefined) {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
if (scope === "public" || scope === "local" || scope === "all") {
|
|
99
|
+
return scope;
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
function validateUserAttributeScope(scope) {
|
|
104
|
+
if (scope === undefined ||
|
|
105
|
+
scope === "public" ||
|
|
106
|
+
scope === "local" ||
|
|
107
|
+
scope === "all") {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
return 'scope must be "public", "local", or "all".';
|
|
111
|
+
}
|
|
40
112
|
function normalizeUserAttributeSelector(selector) {
|
|
41
113
|
const value = typeof selector === "string" ? selector.trim() : "";
|
|
42
114
|
if (!value) {
|
|
@@ -82,7 +154,7 @@ function normalizeUserAttributeMatch(params) {
|
|
|
82
154
|
}
|
|
83
155
|
return 'match.kind must be "tag" or "structured".';
|
|
84
156
|
}
|
|
85
|
-
export function buildP2PTools(mesh, router) {
|
|
157
|
+
export function buildP2PTools(mesh, router, options = {}) {
|
|
86
158
|
return [
|
|
87
159
|
{
|
|
88
160
|
name: "p2p_send_message",
|
|
@@ -294,15 +366,15 @@ export function buildP2PTools(mesh, router) {
|
|
|
294
366
|
try {
|
|
295
367
|
const instances = await router.listInstances();
|
|
296
368
|
const connected = new Set(mesh.getConnectedPeers());
|
|
297
|
-
const rows = instances.map((entry) => ({
|
|
369
|
+
const rows = await Promise.all(instances.map(async (entry) => ({
|
|
298
370
|
...entry,
|
|
371
|
+
userPublicAttributes: entry.userPublicAttributes ?? [],
|
|
299
372
|
connected: connected.has(entry.peerId),
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
.join("\n");
|
|
373
|
+
localLabels: options.peerLabelStore
|
|
374
|
+
? await options.peerLabelStore.listLabels(entry.instanceId)
|
|
375
|
+
: [],
|
|
376
|
+
})));
|
|
377
|
+
const text = formatInstanceList(rows);
|
|
306
378
|
return {
|
|
307
379
|
content: [{ type: "text", text }],
|
|
308
380
|
details: { instances: rows, count: rows.length },
|
|
@@ -442,7 +514,7 @@ export function buildP2PTools(mesh, router) {
|
|
|
442
514
|
{
|
|
443
515
|
name: "p2p_send_user_attribute_message",
|
|
444
516
|
label: "P2P Send User Attribute Message",
|
|
445
|
-
description: 'Send a user message to discovered OpenClaw instances matching
|
|
517
|
+
description: 'Send a user message to discovered OpenClaw instances matching an attribute selector. scope controls the source: "public" for announced user attributes, "local" for local peer labels, or "all" for both; defaults to public. Use selectors like "group=实验室", "project=小龙虾", "tag:P2P", or "#P2P". First run a dry run with dryRun=true to preview targets; if targets match, call again immediately with dryRun=false and the same selector, scope, and message.',
|
|
446
518
|
parameters: {
|
|
447
519
|
type: "object",
|
|
448
520
|
properties: {
|
|
@@ -487,14 +559,20 @@ export function buildP2PTools(mesh, router) {
|
|
|
487
559
|
},
|
|
488
560
|
message: {
|
|
489
561
|
type: "string",
|
|
490
|
-
description: "Message content to send after dry
|
|
562
|
+
description: "Message content to send after a matching dry run.",
|
|
491
563
|
},
|
|
492
564
|
dryRun: {
|
|
493
565
|
type: "boolean",
|
|
494
566
|
description: "Preview matching instances without sending. Run this before group sending.",
|
|
495
567
|
},
|
|
568
|
+
scope: {
|
|
569
|
+
type: "string",
|
|
570
|
+
enum: ["public", "local", "all"],
|
|
571
|
+
description: "Attribute source to match: public announced attributes, local peer labels, or both. Defaults to public.",
|
|
572
|
+
},
|
|
496
573
|
},
|
|
497
|
-
required: ["
|
|
574
|
+
required: ["message"],
|
|
575
|
+
anyOf: [{ required: ["selector"] }, { required: ["match"] }],
|
|
498
576
|
},
|
|
499
577
|
async execute(_toolCallId, params) {
|
|
500
578
|
if (!router) {
|
|
@@ -505,9 +583,15 @@ export function buildP2PTools(mesh, router) {
|
|
|
505
583
|
};
|
|
506
584
|
}
|
|
507
585
|
const match = normalizeUserAttributeMatch(params);
|
|
586
|
+
const scopeError = validateUserAttributeScope(params.scope);
|
|
587
|
+
const scope = normalizeUserAttributeScope(params.scope);
|
|
508
588
|
const message = typeof params.message === "string" ? params.message.trim() : "";
|
|
509
|
-
if (typeof match === "string" || !message) {
|
|
510
|
-
const error = typeof match === "string"
|
|
589
|
+
if (typeof match === "string" || scopeError || !message) {
|
|
590
|
+
const error = typeof match === "string"
|
|
591
|
+
? match
|
|
592
|
+
: scopeError
|
|
593
|
+
? scopeError
|
|
594
|
+
: "message is required.";
|
|
511
595
|
return {
|
|
512
596
|
content: [{ type: "text", text: error }],
|
|
513
597
|
details: { error },
|
|
@@ -515,7 +599,8 @@ export function buildP2PTools(mesh, router) {
|
|
|
515
599
|
};
|
|
516
600
|
}
|
|
517
601
|
const dryRun = params.dryRun === true;
|
|
518
|
-
const
|
|
602
|
+
const options = scope ? { dryRun, scope } : { dryRun };
|
|
603
|
+
const result = await router.sendUserAttributeMessage(match, message, options);
|
|
519
604
|
if (result.error) {
|
|
520
605
|
return {
|
|
521
606
|
content: [{ type: "text", text: result.error }],
|
package/dist/src/inbound.js
CHANGED
|
@@ -35,7 +35,8 @@ export function handleP2PInbound(msg, deps) {
|
|
|
35
35
|
if (!sendToChannel || !msg.payload) {
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
|
-
const
|
|
38
|
+
const sender = msg.instanceId ?? msg.from;
|
|
39
|
+
const text = `[来自 ${sender}]\n${msg.payload}`;
|
|
39
40
|
sendToChannel("libp2p-mesh", msg.from, text).catch((err) => {
|
|
40
41
|
logger?.error?.(`[libp2p-mesh] Failed to forward direct message from ${msg.from}: ${err}`);
|
|
41
42
|
});
|