libp2p-mesh 2026.6.12 → 2026.6.14
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 +161 -0
- package/dist/src/agent-tools.d.ts +120 -1
- package/dist/src/agent-tools.js +153 -0
- package/dist/src/debug-cli.d.ts +21 -0
- package/dist/src/debug-cli.js +54 -0
- package/dist/src/debug-wizard.d.ts +19 -0
- package/dist/src/debug-wizard.js +41 -0
- package/dist/src/instance-peer-store.js +35 -1
- package/dist/src/instance-router.d.ts +2 -8
- package/dist/src/instance-router.js +139 -7
- package/dist/src/plugin.d.ts +7 -0
- package/dist/src/plugin.js +105 -50
- package/dist/src/profile-cli.d.ts +29 -0
- package/dist/src/profile-cli.js +49 -0
- package/dist/src/profile-wizard.d.ts +20 -0
- package/dist/src/profile-wizard.js +141 -0
- package/dist/src/setup-cli.d.ts +19 -0
- package/dist/src/setup-cli.js +32 -28
- package/dist/src/setup-config.d.ts +4 -1
- package/dist/src/setup-config.js +27 -0
- package/dist/src/types.d.ts +67 -0
- package/dist/src/user-attributes.d.ts +6 -0
- package/dist/src/user-attributes.js +92 -0
- package/dist/src/user-md-attributes.d.ts +12 -0
- package/dist/src/user-md-attributes.js +202 -0
- package/dist/src/user-profile-store.d.ts +25 -0
- package/dist/src/user-profile-store.js +187 -0
- package/openclaw.plugin.json +14 -1
- package/package.json +1 -1
- package/src/agent-tools.ts +187 -1
- package/src/debug-cli.ts +93 -0
- package/src/debug-wizard.ts +64 -0
- package/src/instance-peer-store.ts +41 -1
- package/src/instance-router.ts +188 -18
- package/src/plugin.ts +133 -59
- package/src/profile-cli.ts +88 -0
- package/src/profile-wizard.ts +204 -0
- package/src/setup-cli.ts +40 -29
- package/src/setup-config.ts +35 -1
- package/src/types.ts +73 -0
- package/src/user-attributes.ts +122 -0
- package/src/user-md-attributes.ts +256 -0
- package/src/user-profile-store.ts +259 -0
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ P2P mesh network plugin for OpenClaw. Enables direct peer-to-peer communication
|
|
|
10
10
|
- **Bootstrap Mode** — Optional static bootstrap peer list for non-LAN scenarios
|
|
11
11
|
- **WebSocket Transport** — Optional WebSocket support for NAT/firewall-friendly connections
|
|
12
12
|
- **NAT Traversal** — Built-in AutoNAT + UPnP + Circuit Relay v2 + DCUtR for peers behind home routers / firewalls
|
|
13
|
+
- **User Public Attributes** — Announce public tags and structured profile attributes so agents can dry-run and send to locally discovered instances by attribute
|
|
13
14
|
|
|
14
15
|
## Requirements
|
|
15
16
|
|
|
@@ -224,11 +225,34 @@ If `inboundTargets` is an empty array, inbound delivery is disabled. If `inbound
|
|
|
224
225
|
| `relayList` | `string[]` | `[]` | Multiaddrs of relays to reserve a slot on |
|
|
225
226
|
| `discoverRelays` | `number` | `0` | Auto-discover this many relays via content routing |
|
|
226
227
|
| `announceAddrs` | `string[]` | `[]` | Extra multiaddrs to announce on top of auto-detected ones |
|
|
228
|
+
| `announceLogDetail` | `"off" \| "summary" \| "payload"` | `"summary"` | Controls instance announce logging. `summary` logs peer, instance, address count, and attribute count; `payload` also logs the full announce JSON; `off` disables only the new announce summary/payload logs and keeps legacy/basic info logs. |
|
|
227
229
|
| `inboundChannel` | `string` | `undefined` | OpenClaw channel used to display inbound P2P user messages, for example `"feishu"` |
|
|
228
230
|
| `inboundTarget` | `string` | `undefined` | OpenClaw channel target for inbound P2P messages, for example `user:ou_xxx` or `chat:oc_xxx` |
|
|
229
231
|
| `inboundTargets` | `array` | `undefined` | Optional list of receiver-owned channel targets for inbound P2P user messages. When present, it overrides `inboundChannel`/`inboundTarget`; an empty array disables inbound delivery. |
|
|
230
232
|
| `deliveryAckTimeoutMs` | `number` | `15000` | Timeout for waiting on remote channel delivery ACKs |
|
|
231
233
|
|
|
234
|
+
### Announce Startup and Logging
|
|
235
|
+
|
|
236
|
+
During gateway startup, `libp2p-mesh` registers the instance router handlers and direct/broadcast inbound handlers before starting the mesh node. This makes early `instance-announce` messages observable as soon as peers connect, instead of waiting until after mesh startup has already completed.
|
|
237
|
+
|
|
238
|
+
Instance announce logs are controlled by `plugins.entries["libp2p-mesh"].config.announceLogDetail`:
|
|
239
|
+
|
|
240
|
+
- `summary` is the default. It logs send/receive direction, peer ID, instance ID, multiaddr count, and public attribute count. It does not print the full announce JSON.
|
|
241
|
+
- `off` disables the new announce summary and payload logs. It still keeps legacy/basic info logs such as sent announce lines and instance mapping updates, along with warnings and errors.
|
|
242
|
+
- `payload` logs the same summary plus the full announce JSON at debug level.
|
|
243
|
+
|
|
244
|
+
Use the debug command to inspect or change this value:
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
openclaw libp2p-mesh debug
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Full payload logging is intended for short-lived troubleshooting only. Announce payloads can include `userPublicAttributes`, peer multiaddrs, the instance pubkey, and instance identity fields. After changing the setting, restart the gateway for the new logging level to take effect:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
openclaw gateway restart
|
|
254
|
+
```
|
|
255
|
+
|
|
232
256
|
## NAT Traversal
|
|
233
257
|
|
|
234
258
|
When both peers have a routable address (same LAN, public IPs, or working port-forwarding) no extra setup is needed. The defaults above kick in automatically:
|
|
@@ -443,6 +467,129 @@ The sender reports success only after the remote OpenClaw instance forwards the
|
|
|
443
467
|
|
|
444
468
|
Tools are not configured in `openclaw.json`; they are registered automatically by the plugin through `api.registerTool()`.
|
|
445
469
|
|
|
470
|
+
### User public attributes
|
|
471
|
+
|
|
472
|
+
`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
|
+
|
|
474
|
+
There are two sources:
|
|
475
|
+
|
|
476
|
+
- `USER.md` tags are extracted read-only at gateway startup. The plugin never edits `USER.md`.
|
|
477
|
+
- `user-profile.json` stores manually managed structured attributes such as group, project, role, skill, or a custom key.
|
|
478
|
+
|
|
479
|
+
Run the profile wizard to manage structured attributes:
|
|
480
|
+
|
|
481
|
+
```bash
|
|
482
|
+
openclaw libp2p-mesh profile
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
The wizard previews read-only `USER.md` tags and lets you add, edit, or remove only structured profile attributes. Tags extracted from `USER.md` are not written to `user-profile.json`; they are merged in memory with profile attributes and broadcast only in instance announce messages.
|
|
486
|
+
|
|
487
|
+
The default profile path is:
|
|
488
|
+
|
|
489
|
+
```text
|
|
490
|
+
~/.openclaw/libp2p/user-profile.json
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
When `OPENCLAW_STATE_DIR` is set:
|
|
494
|
+
|
|
495
|
+
```text
|
|
496
|
+
$OPENCLAW_STATE_DIR/libp2p/user-profile.json
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
Example `user-profile.json`:
|
|
500
|
+
|
|
501
|
+
```json
|
|
502
|
+
{
|
|
503
|
+
"version": 1,
|
|
504
|
+
"updatedAt": 1782180000000,
|
|
505
|
+
"attributes": [
|
|
506
|
+
{
|
|
507
|
+
"kind": "structured",
|
|
508
|
+
"key": "project",
|
|
509
|
+
"value": "openclaw",
|
|
510
|
+
"label": "project: openclaw",
|
|
511
|
+
"source": "profile"
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
"kind": "structured",
|
|
515
|
+
"key": "role",
|
|
516
|
+
"value": "maintainer",
|
|
517
|
+
"label": "role: maintainer",
|
|
518
|
+
"source": "profile"
|
|
519
|
+
}
|
|
520
|
+
]
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Remote attributes are cached in plugin-managed instance state under `instance-peer.json.userPublicAttributes`:
|
|
525
|
+
|
|
526
|
+
```json
|
|
527
|
+
{
|
|
528
|
+
"version": 1,
|
|
529
|
+
"updatedAt": 1782180000000,
|
|
530
|
+
"instances": {
|
|
531
|
+
"alice-mac@AQIDBAUGBweI.7a3f9e2b": {
|
|
532
|
+
"instanceId": "alice-mac@AQIDBAUGBweI.7a3f9e2b",
|
|
533
|
+
"peerId": "12D3KooW...",
|
|
534
|
+
"instanceName": "alice-mac",
|
|
535
|
+
"multiaddrs": ["/ip4/192.168.1.23/tcp/4001"],
|
|
536
|
+
"userPublicAttributes": [
|
|
537
|
+
{
|
|
538
|
+
"kind": "tag",
|
|
539
|
+
"value": "libp2p",
|
|
540
|
+
"label": "libp2p",
|
|
541
|
+
"source": "USER.md"
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
"kind": "structured",
|
|
545
|
+
"key": "project",
|
|
546
|
+
"value": "openclaw",
|
|
547
|
+
"label": "project: openclaw",
|
|
548
|
+
"source": "profile"
|
|
549
|
+
}
|
|
550
|
+
],
|
|
551
|
+
"lastSeenAt": 1782180000000,
|
|
552
|
+
"lastAnnouncedAt": 1782180000000,
|
|
553
|
+
"source": "announce"
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
Use `p2p_send_user_attribute_message` for attribute-based group messages. Always dry-run first, review the matched instances with the user, then send only after confirmation:
|
|
560
|
+
|
|
561
|
+
```text
|
|
562
|
+
p2p_send_user_attribute_message({
|
|
563
|
+
"match": { "kind": "structured", "key": "project", "value": "openclaw" },
|
|
564
|
+
"message": "今晚同步一下进展",
|
|
565
|
+
"dryRun": true
|
|
566
|
+
})
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
After confirming the dry-run targets:
|
|
570
|
+
|
|
571
|
+
```text
|
|
572
|
+
p2p_send_user_attribute_message({
|
|
573
|
+
"match": { "kind": "structured", "key": "project", "value": "openclaw" },
|
|
574
|
+
"message": "今晚同步一下进展",
|
|
575
|
+
"dryRun": false
|
|
576
|
+
})
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
Tag matches use only the tag value:
|
|
580
|
+
|
|
581
|
+
```text
|
|
582
|
+
p2p_send_user_attribute_message({
|
|
583
|
+
"match": { "kind": "tag", "value": "libp2p" },
|
|
584
|
+
"message": "libp2p 方向有个问题想确认",
|
|
585
|
+
"dryRun": true
|
|
586
|
+
})
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
The first version matches only instances already present in the local `instance-peer.json` discovery cache. It does not search the whole network or ask disconnected peers for more users.
|
|
590
|
+
|
|
591
|
+
Privacy boundary: public attributes are broadcast with instance announce messages to peers your gateway connects to. Do not put private, sensitive, or access-controlled information in `USER.md` tags or `user-profile.json` structured attributes.
|
|
592
|
+
|
|
446
593
|
## Troubleshooting
|
|
447
594
|
|
|
448
595
|
### Peers do not discover each other
|
|
@@ -492,6 +639,20 @@ Peer connection and disconnection are logged at `info` level:
|
|
|
492
639
|
|
|
493
640
|
If these lines are missing, confirm the gateway is running with normal info logs enabled and that both instances are on the same mDNS, bootstrap, or relay network.
|
|
494
641
|
|
|
642
|
+
### Instance announce routes are missing between two machines
|
|
643
|
+
|
|
644
|
+
If peers connect but sending by OpenClaw instance ID fails or `instance-peer.json` is not updated, first confirm both gateways were restarted after the latest config change. On startup, the gateway now attaches the instance router plus inbound message handlers before starting the mesh, so early announces should be handled once the peer connection appears.
|
|
645
|
+
|
|
646
|
+
For a short debug session on both computers:
|
|
647
|
+
|
|
648
|
+
1. Run `openclaw libp2p-mesh debug`.
|
|
649
|
+
2. Set `announceLogDetail` to `payload` and confirm the privacy warning.
|
|
650
|
+
3. Restart both gateways with `openclaw gateway restart`.
|
|
651
|
+
4. Watch for summary lines and debug lines containing full announce payload JSON.
|
|
652
|
+
5. Return to `summary` or `off` with `openclaw libp2p-mesh debug`, then restart again.
|
|
653
|
+
|
|
654
|
+
Full payload logs may expose `userPublicAttributes`, multiaddrs, pubkey, and instance identity, so avoid sharing these logs outside the debugging context.
|
|
655
|
+
|
|
495
656
|
## Architecture
|
|
496
657
|
|
|
497
658
|
```
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import type { DeliveryTargetResult, InstanceRouter, MeshNetwork } from "./types.js";
|
|
1
|
+
import type { DeliveryTargetResult, InstanceRouter, MeshNetwork, UserPublicAttribute } from "./types.js";
|
|
2
|
+
type SendUserAttributeToolParams = {
|
|
3
|
+
match?: {
|
|
4
|
+
kind?: unknown;
|
|
5
|
+
key?: unknown;
|
|
6
|
+
value?: unknown;
|
|
7
|
+
};
|
|
8
|
+
message?: unknown;
|
|
9
|
+
dryRun?: unknown;
|
|
10
|
+
};
|
|
2
11
|
export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter): ({
|
|
3
12
|
name: string;
|
|
4
13
|
label: string;
|
|
@@ -16,6 +25,8 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
16
25
|
};
|
|
17
26
|
topic?: undefined;
|
|
18
27
|
instanceId?: undefined;
|
|
28
|
+
match?: undefined;
|
|
29
|
+
dryRun?: undefined;
|
|
19
30
|
};
|
|
20
31
|
required: string[];
|
|
21
32
|
};
|
|
@@ -62,6 +73,8 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
62
73
|
};
|
|
63
74
|
peerId?: undefined;
|
|
64
75
|
instanceId?: undefined;
|
|
76
|
+
match?: undefined;
|
|
77
|
+
dryRun?: undefined;
|
|
65
78
|
};
|
|
66
79
|
required: string[];
|
|
67
80
|
};
|
|
@@ -102,6 +115,8 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
102
115
|
message?: undefined;
|
|
103
116
|
topic?: undefined;
|
|
104
117
|
instanceId?: undefined;
|
|
118
|
+
match?: undefined;
|
|
119
|
+
dryRun?: undefined;
|
|
105
120
|
};
|
|
106
121
|
required?: undefined;
|
|
107
122
|
};
|
|
@@ -141,6 +156,8 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
141
156
|
message?: undefined;
|
|
142
157
|
topic?: undefined;
|
|
143
158
|
instanceId?: undefined;
|
|
159
|
+
match?: undefined;
|
|
160
|
+
dryRun?: undefined;
|
|
144
161
|
};
|
|
145
162
|
required?: undefined;
|
|
146
163
|
};
|
|
@@ -189,6 +206,8 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
189
206
|
message?: undefined;
|
|
190
207
|
topic?: undefined;
|
|
191
208
|
instanceId?: undefined;
|
|
209
|
+
match?: undefined;
|
|
210
|
+
dryRun?: undefined;
|
|
192
211
|
};
|
|
193
212
|
required?: undefined;
|
|
194
213
|
};
|
|
@@ -230,6 +249,8 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
230
249
|
message?: undefined;
|
|
231
250
|
topic?: undefined;
|
|
232
251
|
instanceId?: undefined;
|
|
252
|
+
match?: undefined;
|
|
253
|
+
dryRun?: undefined;
|
|
233
254
|
};
|
|
234
255
|
required?: undefined;
|
|
235
256
|
};
|
|
@@ -258,6 +279,7 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
258
279
|
instanceName?: string;
|
|
259
280
|
multiaddrs: string[];
|
|
260
281
|
pubkey?: string;
|
|
282
|
+
userPublicAttributes?: UserPublicAttribute[];
|
|
261
283
|
lastSeenAt: number;
|
|
262
284
|
lastAnnouncedAt: number;
|
|
263
285
|
source: "announce";
|
|
@@ -294,6 +316,8 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
294
316
|
peerId?: undefined;
|
|
295
317
|
message?: undefined;
|
|
296
318
|
topic?: undefined;
|
|
319
|
+
match?: undefined;
|
|
320
|
+
dryRun?: undefined;
|
|
297
321
|
};
|
|
298
322
|
required: string[];
|
|
299
323
|
};
|
|
@@ -369,6 +393,8 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
369
393
|
};
|
|
370
394
|
peerId?: undefined;
|
|
371
395
|
topic?: undefined;
|
|
396
|
+
match?: undefined;
|
|
397
|
+
dryRun?: undefined;
|
|
372
398
|
};
|
|
373
399
|
required: string[];
|
|
374
400
|
};
|
|
@@ -430,4 +456,97 @@ export declare function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter
|
|
|
430
456
|
};
|
|
431
457
|
isError?: undefined;
|
|
432
458
|
}>;
|
|
459
|
+
} | {
|
|
460
|
+
name: string;
|
|
461
|
+
label: string;
|
|
462
|
+
description: string;
|
|
463
|
+
parameters: {
|
|
464
|
+
type: "object";
|
|
465
|
+
properties: {
|
|
466
|
+
match: {
|
|
467
|
+
type: "object";
|
|
468
|
+
description: string;
|
|
469
|
+
oneOf: ({
|
|
470
|
+
type: "object";
|
|
471
|
+
additionalProperties: boolean;
|
|
472
|
+
properties: {
|
|
473
|
+
kind: {
|
|
474
|
+
const: "tag";
|
|
475
|
+
};
|
|
476
|
+
value: {
|
|
477
|
+
type: "string";
|
|
478
|
+
description: string;
|
|
479
|
+
};
|
|
480
|
+
key?: undefined;
|
|
481
|
+
};
|
|
482
|
+
required: string[];
|
|
483
|
+
} | {
|
|
484
|
+
type: "object";
|
|
485
|
+
additionalProperties: boolean;
|
|
486
|
+
properties: {
|
|
487
|
+
kind: {
|
|
488
|
+
const: "structured";
|
|
489
|
+
};
|
|
490
|
+
key: {
|
|
491
|
+
type: "string";
|
|
492
|
+
description: string;
|
|
493
|
+
};
|
|
494
|
+
value: {
|
|
495
|
+
type: "string";
|
|
496
|
+
description: string;
|
|
497
|
+
};
|
|
498
|
+
};
|
|
499
|
+
required: string[];
|
|
500
|
+
})[];
|
|
501
|
+
};
|
|
502
|
+
message: {
|
|
503
|
+
type: "string";
|
|
504
|
+
description: string;
|
|
505
|
+
};
|
|
506
|
+
dryRun: {
|
|
507
|
+
type: "boolean";
|
|
508
|
+
description: string;
|
|
509
|
+
};
|
|
510
|
+
peerId?: undefined;
|
|
511
|
+
topic?: undefined;
|
|
512
|
+
instanceId?: undefined;
|
|
513
|
+
};
|
|
514
|
+
required: string[];
|
|
515
|
+
};
|
|
516
|
+
execute(_toolCallId: string, params: SendUserAttributeToolParams): Promise<{
|
|
517
|
+
content: {
|
|
518
|
+
type: "text";
|
|
519
|
+
text: string;
|
|
520
|
+
}[];
|
|
521
|
+
details: {
|
|
522
|
+
initialized: boolean;
|
|
523
|
+
error?: undefined;
|
|
524
|
+
};
|
|
525
|
+
isError: boolean;
|
|
526
|
+
} | {
|
|
527
|
+
content: {
|
|
528
|
+
type: "text";
|
|
529
|
+
text: string;
|
|
530
|
+
}[];
|
|
531
|
+
details: {
|
|
532
|
+
error: string;
|
|
533
|
+
initialized?: undefined;
|
|
534
|
+
};
|
|
535
|
+
isError: boolean;
|
|
536
|
+
} | {
|
|
537
|
+
content: {
|
|
538
|
+
type: "text";
|
|
539
|
+
text: string;
|
|
540
|
+
}[];
|
|
541
|
+
details: import("./types.js").UserAttributeMessageResult;
|
|
542
|
+
isError?: undefined;
|
|
543
|
+
} | {
|
|
544
|
+
content: {
|
|
545
|
+
type: "text";
|
|
546
|
+
text: string;
|
|
547
|
+
}[];
|
|
548
|
+
details: import("./types.js").UserAttributeMessageResult;
|
|
549
|
+
isError: boolean | undefined;
|
|
550
|
+
}>;
|
|
433
551
|
})[];
|
|
552
|
+
export {};
|
package/dist/src/agent-tools.js
CHANGED
|
@@ -12,6 +12,49 @@ function formatDeliveryResults(instanceId, delivered, results) {
|
|
|
12
12
|
});
|
|
13
13
|
return [heading, ...lines].join("\n");
|
|
14
14
|
}
|
|
15
|
+
function attributeLabel(attribute) {
|
|
16
|
+
if (attribute.kind === "tag") {
|
|
17
|
+
return `tag:${attribute.value}`;
|
|
18
|
+
}
|
|
19
|
+
return `${attribute.key}:${attribute.value}`;
|
|
20
|
+
}
|
|
21
|
+
function instanceTargetLabel(target) {
|
|
22
|
+
const name = target.instanceName ? ` (${target.instanceName})` : "";
|
|
23
|
+
return `${target.instanceId}${name} -> ${target.peerId}`;
|
|
24
|
+
}
|
|
25
|
+
function formatUserAttributeTargets(targets) {
|
|
26
|
+
return targets.map((target) => `${instanceTargetLabel(target)} [${attributeLabel(target.matchedAttribute)}]`);
|
|
27
|
+
}
|
|
28
|
+
function formatUserAttributeResults(results) {
|
|
29
|
+
return results.map((result) => {
|
|
30
|
+
let status = "已送达";
|
|
31
|
+
if (!result.sent) {
|
|
32
|
+
status = `发送失败:${result.error ?? "unknown error"}`;
|
|
33
|
+
}
|
|
34
|
+
else if (!result.delivered) {
|
|
35
|
+
status = `投递失败:${result.error ?? "unknown error"}`;
|
|
36
|
+
}
|
|
37
|
+
return `${instanceTargetLabel(result)}:${status}`;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function normalizeUserAttributeMatch(params) {
|
|
41
|
+
const match = params.match;
|
|
42
|
+
if (!match || typeof match !== "object") {
|
|
43
|
+
return "match is required.";
|
|
44
|
+
}
|
|
45
|
+
if (match.kind === "tag") {
|
|
46
|
+
const value = typeof match.value === "string" ? match.value.trim() : "";
|
|
47
|
+
return value ? { kind: "tag", value } : "match.value is required for tag matches.";
|
|
48
|
+
}
|
|
49
|
+
if (match.kind === "structured") {
|
|
50
|
+
const key = typeof match.key === "string" ? match.key.trim() : "";
|
|
51
|
+
const value = typeof match.value === "string" ? match.value.trim() : "";
|
|
52
|
+
return key && value
|
|
53
|
+
? { kind: "structured", key, value }
|
|
54
|
+
: "match.key and match.value are required for structured matches.";
|
|
55
|
+
}
|
|
56
|
+
return 'match.kind must be "tag" or "structured".';
|
|
57
|
+
}
|
|
15
58
|
export function buildP2PTools(mesh, router) {
|
|
16
59
|
return [
|
|
17
60
|
{
|
|
@@ -369,5 +412,115 @@ export function buildP2PTools(mesh, router) {
|
|
|
369
412
|
};
|
|
370
413
|
},
|
|
371
414
|
},
|
|
415
|
+
{
|
|
416
|
+
name: "p2p_send_user_attribute_message",
|
|
417
|
+
label: "P2P Send User Attribute Message",
|
|
418
|
+
description: "Send a user message to discovered OpenClaw instances matching a public user attribute. Always run a dry run with dryRun=true first and ask the user to confirm targets before group sending.",
|
|
419
|
+
parameters: {
|
|
420
|
+
type: "object",
|
|
421
|
+
properties: {
|
|
422
|
+
match: {
|
|
423
|
+
type: "object",
|
|
424
|
+
description: 'User public attribute match. Use { "kind": "tag", "value": "..." } for USER.md tags or { "kind": "structured", "key": "...", "value": "..." } for profile attributes.',
|
|
425
|
+
oneOf: [
|
|
426
|
+
{
|
|
427
|
+
type: "object",
|
|
428
|
+
additionalProperties: false,
|
|
429
|
+
properties: {
|
|
430
|
+
kind: { const: "tag" },
|
|
431
|
+
value: {
|
|
432
|
+
type: "string",
|
|
433
|
+
description: "Tag value to match.",
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
required: ["kind", "value"],
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
type: "object",
|
|
440
|
+
additionalProperties: false,
|
|
441
|
+
properties: {
|
|
442
|
+
kind: { const: "structured" },
|
|
443
|
+
key: {
|
|
444
|
+
type: "string",
|
|
445
|
+
description: "Structured attribute key to match.",
|
|
446
|
+
},
|
|
447
|
+
value: {
|
|
448
|
+
type: "string",
|
|
449
|
+
description: "Structured attribute value to match.",
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
required: ["kind", "key", "value"],
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
},
|
|
456
|
+
message: {
|
|
457
|
+
type: "string",
|
|
458
|
+
description: "Message content to send after dry-run target confirmation.",
|
|
459
|
+
},
|
|
460
|
+
dryRun: {
|
|
461
|
+
type: "boolean",
|
|
462
|
+
description: "Preview matching instances without sending. Run this before group sending.",
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
required: ["match", "message"],
|
|
466
|
+
},
|
|
467
|
+
async execute(_toolCallId, params) {
|
|
468
|
+
if (!router) {
|
|
469
|
+
return {
|
|
470
|
+
content: [{ type: "text", text: "Instance router is not initialized." }],
|
|
471
|
+
details: { initialized: false },
|
|
472
|
+
isError: true,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
const match = normalizeUserAttributeMatch(params);
|
|
476
|
+
const message = typeof params.message === "string" ? params.message.trim() : "";
|
|
477
|
+
if (typeof match === "string" || !message) {
|
|
478
|
+
const error = typeof match === "string" ? match : "message is required.";
|
|
479
|
+
return {
|
|
480
|
+
content: [{ type: "text", text: error }],
|
|
481
|
+
details: { error },
|
|
482
|
+
isError: true,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
const dryRun = params.dryRun === true;
|
|
486
|
+
const result = await router.sendUserAttributeMessage(match, message, { dryRun });
|
|
487
|
+
if (result.error) {
|
|
488
|
+
return {
|
|
489
|
+
content: [{ type: "text", text: result.error }],
|
|
490
|
+
details: result,
|
|
491
|
+
isError: true,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
if (dryRun) {
|
|
495
|
+
const targetLines = formatUserAttributeTargets(result.targets ?? []);
|
|
496
|
+
return {
|
|
497
|
+
content: [
|
|
498
|
+
{
|
|
499
|
+
type: "text",
|
|
500
|
+
text: [
|
|
501
|
+
`Dry run matched ${result.matched} instance(s). No message was sent.`,
|
|
502
|
+
...targetLines,
|
|
503
|
+
].join("\n"),
|
|
504
|
+
},
|
|
505
|
+
],
|
|
506
|
+
details: result,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
const resultLines = formatUserAttributeResults(result.results ?? []);
|
|
510
|
+
return {
|
|
511
|
+
content: [
|
|
512
|
+
{
|
|
513
|
+
type: "text",
|
|
514
|
+
text: [
|
|
515
|
+
`Matched ${result.matched} instance(s); sent ${result.sent}; delivered ${result.delivered}; failed ${result.failed}.`,
|
|
516
|
+
...resultLines,
|
|
517
|
+
].join("\n"),
|
|
518
|
+
},
|
|
519
|
+
],
|
|
520
|
+
details: result,
|
|
521
|
+
isError: result.failed > 0 ? true : undefined,
|
|
522
|
+
};
|
|
523
|
+
},
|
|
524
|
+
},
|
|
372
525
|
];
|
|
373
526
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import type { OpenClawPluginCliContext } from "openclaw/plugin-sdk/plugin-runtime";
|
|
3
|
+
import type { SetupPrompter } from "./setup-wizard.js";
|
|
4
|
+
import type { AnnounceLogDetail } from "./types.js";
|
|
5
|
+
type CliRootCommand = {
|
|
6
|
+
command(name: string): {
|
|
7
|
+
description(text: string): {
|
|
8
|
+
action(handler: () => Promise<void>): void;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export type DebugConfigWriter = {
|
|
13
|
+
saveAnnounceLogDetail(detail: AnnounceLogDetail): Promise<void>;
|
|
14
|
+
};
|
|
15
|
+
export type DebugCliDeps = {
|
|
16
|
+
createPrompter?: (ctx: OpenClawPluginCliContext) => SetupPrompter;
|
|
17
|
+
createWriter?: (api: OpenClawPluginApi) => DebugConfigWriter;
|
|
18
|
+
};
|
|
19
|
+
export declare function registerLibp2pMeshDebugCli(api: OpenClawPluginApi, deps?: DebugCliDeps): void;
|
|
20
|
+
export declare function registerLibp2pMeshDebugCommand(root: CliRootCommand, api: OpenClawPluginApi, ctx: OpenClawPluginCliContext, deps?: DebugCliDeps): void;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createReadlinePrompter, LIBP2P_MESH_CLI_REGISTRATION, } from "./setup-cli.js";
|
|
2
|
+
import { applyAnnounceLogDetail, getAnnounceLogDetail, } from "./setup-config.js";
|
|
3
|
+
import { runDebugWizard } from "./debug-wizard.js";
|
|
4
|
+
const DEBUG_CLI_AFTER_WRITE = {
|
|
5
|
+
mode: "none",
|
|
6
|
+
reason: "libp2p-mesh debug config updated; restart manually to apply gateway changes.",
|
|
7
|
+
};
|
|
8
|
+
export function registerLibp2pMeshDebugCli(api, deps = {}) {
|
|
9
|
+
api.registerCli((ctx) => {
|
|
10
|
+
const root = ctx.program
|
|
11
|
+
.command("libp2p-mesh")
|
|
12
|
+
.description("Configure libp2p-mesh plugin.");
|
|
13
|
+
registerLibp2pMeshDebugCommand(root, api, ctx, deps);
|
|
14
|
+
}, LIBP2P_MESH_CLI_REGISTRATION);
|
|
15
|
+
}
|
|
16
|
+
export function registerLibp2pMeshDebugCommand(root, api, ctx, deps = {}) {
|
|
17
|
+
root
|
|
18
|
+
.command("debug")
|
|
19
|
+
.description("Manage libp2p-mesh debug logging config.")
|
|
20
|
+
.action(async () => {
|
|
21
|
+
const prompter = (deps.createPrompter?.(ctx) ?? createReadlinePrompter());
|
|
22
|
+
const writer = deps.createWriter?.(api) ?? createOpenClawDebugConfigWriter(api);
|
|
23
|
+
try {
|
|
24
|
+
const result = await runDebugWizard({
|
|
25
|
+
prompter,
|
|
26
|
+
current: getAnnounceLogDetail(ctx.config),
|
|
27
|
+
writer,
|
|
28
|
+
});
|
|
29
|
+
prompter.print(result.message);
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
prompter.close?.();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function createOpenClawDebugConfigWriter(api) {
|
|
37
|
+
return {
|
|
38
|
+
async saveAnnounceLogDetail(detail) {
|
|
39
|
+
await api.runtime.config.mutateConfigFile({
|
|
40
|
+
afterWrite: DEBUG_CLI_AFTER_WRITE,
|
|
41
|
+
mutate(draft) {
|
|
42
|
+
const nextConfig = applyAnnounceLogDetail(draft, detail);
|
|
43
|
+
replaceConfig(draft, nextConfig);
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function replaceConfig(draft, nextConfig) {
|
|
50
|
+
for (const key of Object.keys(draft)) {
|
|
51
|
+
delete draft[key];
|
|
52
|
+
}
|
|
53
|
+
Object.assign(draft, structuredClone(nextConfig));
|
|
54
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type SetupPrompter } from "./setup-wizard.js";
|
|
2
|
+
import type { AnnounceLogDetail } from "./types.js";
|
|
3
|
+
export type DebugPromptChoice = AnnounceLogDetail | "cancel";
|
|
4
|
+
export type RunDebugWizardOptions = {
|
|
5
|
+
prompter: SetupPrompter;
|
|
6
|
+
current: AnnounceLogDetail;
|
|
7
|
+
writer: {
|
|
8
|
+
saveAnnounceLogDetail(detail: AnnounceLogDetail): Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
export type DebugWizardResult = {
|
|
12
|
+
status: "saved";
|
|
13
|
+
announceLogDetail: AnnounceLogDetail;
|
|
14
|
+
message: string;
|
|
15
|
+
} | {
|
|
16
|
+
status: "cancelled";
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
19
|
+
export declare function runDebugWizard(options: RunDebugWizardOptions): Promise<DebugWizardResult>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { SetupCancelledError } from "./setup-wizard.js";
|
|
2
|
+
const CANCELLED_MESSAGE = "Debug configuration cancelled. No changes were written.";
|
|
3
|
+
const SAVED_MESSAGE = "Debug config updated.\n\nRestart the gateway to apply changes:\nopenclaw gateway restart";
|
|
4
|
+
export async function runDebugWizard(options) {
|
|
5
|
+
try {
|
|
6
|
+
options.prompter.print(`Current announceLogDetail: ${options.current}`);
|
|
7
|
+
const selected = await options.prompter.select("Set announceLogDetail:", [
|
|
8
|
+
{ label: "summary: log peer, instance, address and attribute counts", value: "summary" },
|
|
9
|
+
{ label: "off: disable announce summary/payload logs", value: "off" },
|
|
10
|
+
{ label: "payload: log full announce JSON", value: "payload" },
|
|
11
|
+
{ label: "Cancel", value: "cancel" },
|
|
12
|
+
]);
|
|
13
|
+
if (selected === "cancel") {
|
|
14
|
+
return cancelledResult();
|
|
15
|
+
}
|
|
16
|
+
if (selected === "payload") {
|
|
17
|
+
const confirmed = await options.prompter.confirm("Full announce payload logs may include userPublicAttributes, multiaddrs, pubkey and instance identity. Enable payload logs?", false);
|
|
18
|
+
if (!confirmed) {
|
|
19
|
+
return cancelledResult();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
await options.writer.saveAnnounceLogDetail(selected);
|
|
23
|
+
return {
|
|
24
|
+
status: "saved",
|
|
25
|
+
announceLogDetail: selected,
|
|
26
|
+
message: SAVED_MESSAGE,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error instanceof SetupCancelledError) {
|
|
31
|
+
return cancelledResult();
|
|
32
|
+
}
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function cancelledResult() {
|
|
37
|
+
return {
|
|
38
|
+
status: "cancelled",
|
|
39
|
+
message: CANCELLED_MESSAGE,
|
|
40
|
+
};
|
|
41
|
+
}
|