libp2p-mesh 2026.6.12 → 2026.6.13
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 +124 -0
- package/dist/src/agent-tools.d.ts +120 -1
- package/dist/src/agent-tools.js +153 -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 +94 -2
- package/dist/src/plugin.js +8 -2
- package/dist/src/profile-cli.d.ts +27 -0
- package/dist/src/profile-cli.js +47 -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/types.d.ts +63 -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 +2 -1
- package/package.json +1 -1
- package/src/agent-tools.ts +187 -1
- package/src/instance-peer-store.ts +41 -1
- package/src/instance-router.ts +121 -12
- package/src/plugin.ts +8 -2
- package/src/profile-cli.ts +85 -0
- package/src/profile-wizard.ts +204 -0
- package/src/setup-cli.ts +40 -29
- package/src/types.ts +68 -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/src/agent-tools.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
DeliveryTargetResult,
|
|
3
|
+
InstanceRouter,
|
|
4
|
+
MeshNetwork,
|
|
5
|
+
UserAttributeMatch,
|
|
6
|
+
UserAttributeMessageDeliveryResult,
|
|
7
|
+
UserAttributeMessageTarget,
|
|
8
|
+
UserPublicAttribute,
|
|
9
|
+
} from "./types.js";
|
|
2
10
|
|
|
3
11
|
function targetLabel(result: DeliveryTargetResult) {
|
|
4
12
|
const name = result.id ?? `${result.channel}:${result.target}`;
|
|
@@ -20,6 +28,68 @@ function formatDeliveryResults(
|
|
|
20
28
|
return [heading, ...lines].join("\n");
|
|
21
29
|
}
|
|
22
30
|
|
|
31
|
+
function attributeLabel(attribute: UserPublicAttribute): string {
|
|
32
|
+
if (attribute.kind === "tag") {
|
|
33
|
+
return `tag:${attribute.value}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return `${attribute.key}:${attribute.value}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function instanceTargetLabel(target: Pick<UserAttributeMessageTarget, "instanceId" | "instanceName" | "peerId">) {
|
|
40
|
+
const name = target.instanceName ? ` (${target.instanceName})` : "";
|
|
41
|
+
return `${target.instanceId}${name} -> ${target.peerId}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatUserAttributeTargets(targets: UserAttributeMessageTarget[]) {
|
|
45
|
+
return targets.map((target) => `${instanceTargetLabel(target)} [${attributeLabel(target.matchedAttribute)}]`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function formatUserAttributeResults(results: UserAttributeMessageDeliveryResult[]) {
|
|
49
|
+
return results.map((result) => {
|
|
50
|
+
let status = "已送达";
|
|
51
|
+
if (!result.sent) {
|
|
52
|
+
status = `发送失败:${result.error ?? "unknown error"}`;
|
|
53
|
+
} else if (!result.delivered) {
|
|
54
|
+
status = `投递失败:${result.error ?? "unknown error"}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return `${instanceTargetLabel(result)}:${status}`;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type SendUserAttributeToolParams = {
|
|
62
|
+
match?: {
|
|
63
|
+
kind?: unknown;
|
|
64
|
+
key?: unknown;
|
|
65
|
+
value?: unknown;
|
|
66
|
+
};
|
|
67
|
+
message?: unknown;
|
|
68
|
+
dryRun?: unknown;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function normalizeUserAttributeMatch(params: SendUserAttributeToolParams): UserAttributeMatch | string {
|
|
72
|
+
const match = params.match;
|
|
73
|
+
if (!match || typeof match !== "object") {
|
|
74
|
+
return "match is required.";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (match.kind === "tag") {
|
|
78
|
+
const value = typeof match.value === "string" ? match.value.trim() : "";
|
|
79
|
+
return value ? { kind: "tag", value } : "match.value is required for tag matches.";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (match.kind === "structured") {
|
|
83
|
+
const key = typeof match.key === "string" ? match.key.trim() : "";
|
|
84
|
+
const value = typeof match.value === "string" ? match.value.trim() : "";
|
|
85
|
+
return key && value
|
|
86
|
+
? { kind: "structured", key, value }
|
|
87
|
+
: "match.key and match.value are required for structured matches.";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return 'match.kind must be "tag" or "structured".';
|
|
91
|
+
}
|
|
92
|
+
|
|
23
93
|
export function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter) {
|
|
24
94
|
return [
|
|
25
95
|
{
|
|
@@ -378,5 +448,121 @@ export function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter) {
|
|
|
378
448
|
};
|
|
379
449
|
},
|
|
380
450
|
},
|
|
451
|
+
{
|
|
452
|
+
name: "p2p_send_user_attribute_message",
|
|
453
|
+
label: "P2P Send User Attribute Message",
|
|
454
|
+
description:
|
|
455
|
+
"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.",
|
|
456
|
+
parameters: {
|
|
457
|
+
type: "object" as const,
|
|
458
|
+
properties: {
|
|
459
|
+
match: {
|
|
460
|
+
type: "object" as const,
|
|
461
|
+
description:
|
|
462
|
+
'User public attribute match. Use { "kind": "tag", "value": "..." } for USER.md tags or { "kind": "structured", "key": "...", "value": "..." } for profile attributes.',
|
|
463
|
+
oneOf: [
|
|
464
|
+
{
|
|
465
|
+
type: "object" as const,
|
|
466
|
+
additionalProperties: false,
|
|
467
|
+
properties: {
|
|
468
|
+
kind: { const: "tag" as const },
|
|
469
|
+
value: {
|
|
470
|
+
type: "string" as const,
|
|
471
|
+
description: "Tag value to match.",
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
required: ["kind", "value"],
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
type: "object" as const,
|
|
478
|
+
additionalProperties: false,
|
|
479
|
+
properties: {
|
|
480
|
+
kind: { const: "structured" as const },
|
|
481
|
+
key: {
|
|
482
|
+
type: "string" as const,
|
|
483
|
+
description: "Structured attribute key to match.",
|
|
484
|
+
},
|
|
485
|
+
value: {
|
|
486
|
+
type: "string" as const,
|
|
487
|
+
description: "Structured attribute value to match.",
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
required: ["kind", "key", "value"],
|
|
491
|
+
},
|
|
492
|
+
],
|
|
493
|
+
},
|
|
494
|
+
message: {
|
|
495
|
+
type: "string" as const,
|
|
496
|
+
description: "Message content to send after dry-run target confirmation.",
|
|
497
|
+
},
|
|
498
|
+
dryRun: {
|
|
499
|
+
type: "boolean" as const,
|
|
500
|
+
description: "Preview matching instances without sending. Run this before group sending.",
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
required: ["match", "message"],
|
|
504
|
+
},
|
|
505
|
+
async execute(_toolCallId: string, params: SendUserAttributeToolParams) {
|
|
506
|
+
if (!router) {
|
|
507
|
+
return {
|
|
508
|
+
content: [{ type: "text" as const, text: "Instance router is not initialized." }],
|
|
509
|
+
details: { initialized: false },
|
|
510
|
+
isError: true,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const match = normalizeUserAttributeMatch(params);
|
|
515
|
+
const message = typeof params.message === "string" ? params.message.trim() : "";
|
|
516
|
+
if (typeof match === "string" || !message) {
|
|
517
|
+
const error = typeof match === "string" ? match : "message is required.";
|
|
518
|
+
return {
|
|
519
|
+
content: [{ type: "text" as const, text: error }],
|
|
520
|
+
details: { error },
|
|
521
|
+
isError: true,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const dryRun = params.dryRun === true;
|
|
526
|
+
const result = await router.sendUserAttributeMessage(match, message, { dryRun });
|
|
527
|
+
if (result.error) {
|
|
528
|
+
return {
|
|
529
|
+
content: [{ type: "text" as const, text: result.error }],
|
|
530
|
+
details: result,
|
|
531
|
+
isError: true,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (dryRun) {
|
|
536
|
+
const targetLines = formatUserAttributeTargets(result.targets ?? []);
|
|
537
|
+
return {
|
|
538
|
+
content: [
|
|
539
|
+
{
|
|
540
|
+
type: "text" as const,
|
|
541
|
+
text: [
|
|
542
|
+
`Dry run matched ${result.matched} instance(s). No message was sent.`,
|
|
543
|
+
...targetLines,
|
|
544
|
+
].join("\n"),
|
|
545
|
+
},
|
|
546
|
+
],
|
|
547
|
+
details: result,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const resultLines = formatUserAttributeResults(result.results ?? []);
|
|
552
|
+
return {
|
|
553
|
+
content: [
|
|
554
|
+
{
|
|
555
|
+
type: "text" as const,
|
|
556
|
+
text: [
|
|
557
|
+
`Matched ${result.matched} instance(s); sent ${result.sent}; delivered ${result.delivered}; failed ${result.failed}.`,
|
|
558
|
+
...resultLines,
|
|
559
|
+
].join("\n"),
|
|
560
|
+
},
|
|
561
|
+
],
|
|
562
|
+
details: result,
|
|
563
|
+
isError: result.failed > 0 ? true : undefined,
|
|
564
|
+
};
|
|
565
|
+
},
|
|
566
|
+
},
|
|
381
567
|
];
|
|
382
568
|
}
|
|
@@ -6,7 +6,9 @@ import type {
|
|
|
6
6
|
InstancePeerRecord,
|
|
7
7
|
InstancePeerStore,
|
|
8
8
|
InstancePeerTable,
|
|
9
|
+
UserPublicAttribute,
|
|
9
10
|
} from "./types.js";
|
|
11
|
+
import { normalizeUserPublicAttribute } from "./user-attributes.js";
|
|
10
12
|
|
|
11
13
|
export interface StoreLogger {
|
|
12
14
|
info?(message: string): void;
|
|
@@ -38,21 +40,50 @@ function sameStringArray(a: string[] = [], b: string[] = []): boolean {
|
|
|
38
40
|
return a.every((value, index) => value === b[index]);
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
function normalizeUserPublicAttributes(value: unknown): UserPublicAttribute[] {
|
|
44
|
+
if (!Array.isArray(value)) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return value
|
|
49
|
+
.map((attribute) => normalizeUserPublicAttribute(attribute))
|
|
50
|
+
.filter((attribute): attribute is UserPublicAttribute => attribute !== undefined);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sameUserPublicAttributes(a: UserPublicAttribute[] = [], b: UserPublicAttribute[] = []): boolean {
|
|
54
|
+
if (a.length !== b.length) return false;
|
|
55
|
+
return a.every((attribute, index) => {
|
|
56
|
+
const other = b[index];
|
|
57
|
+
return JSON.stringify(attribute) === JSON.stringify(other);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
41
61
|
function sameRecord(
|
|
42
62
|
record: InstancePeerRecord | undefined,
|
|
43
63
|
payload: InstanceAnnouncePayload,
|
|
44
64
|
): boolean {
|
|
45
65
|
if (!record) return false;
|
|
46
66
|
|
|
67
|
+
const recordAttributes = normalizeUserPublicAttributes(record.userPublicAttributes);
|
|
68
|
+
const payloadAttributes = normalizeUserPublicAttributes(payload.userPublicAttributes);
|
|
69
|
+
|
|
47
70
|
return (
|
|
48
71
|
record.peerId === payload.peerId &&
|
|
49
72
|
record.instanceName === payload.instanceName &&
|
|
50
73
|
record.pubkey === payload.pubkey &&
|
|
51
74
|
sameStringArray(record.multiaddrs, payload.multiaddrs) &&
|
|
75
|
+
sameUserPublicAttributes(recordAttributes, payloadAttributes) &&
|
|
52
76
|
record.lastAnnouncedAt === payload.announcedAt
|
|
53
77
|
);
|
|
54
78
|
}
|
|
55
79
|
|
|
80
|
+
function normalizeRecord(value: InstancePeerRecord): InstancePeerRecord {
|
|
81
|
+
return {
|
|
82
|
+
...value,
|
|
83
|
+
userPublicAttributes: normalizeUserPublicAttributes(value.userPublicAttributes),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
56
87
|
function normalizeTable(value: unknown): InstancePeerTable {
|
|
57
88
|
const candidate = value && typeof value === "object" ? value : {};
|
|
58
89
|
const table = candidate as Partial<InstancePeerTable>;
|
|
@@ -61,10 +92,17 @@ function normalizeTable(value: unknown): InstancePeerTable {
|
|
|
61
92
|
? table.instances
|
|
62
93
|
: {};
|
|
63
94
|
|
|
95
|
+
const normalizedInstances: Record<string, InstancePeerRecord> = {};
|
|
96
|
+
for (const [instanceId, record] of Object.entries(instances)) {
|
|
97
|
+
if (record && typeof record === "object") {
|
|
98
|
+
normalizedInstances[instanceId] = normalizeRecord(record as InstancePeerRecord);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
64
102
|
return {
|
|
65
103
|
version: 1,
|
|
66
104
|
updatedAt: typeof table.updatedAt === "number" ? table.updatedAt : Date.now(),
|
|
67
|
-
instances:
|
|
105
|
+
instances: normalizedInstances,
|
|
68
106
|
};
|
|
69
107
|
}
|
|
70
108
|
|
|
@@ -149,6 +187,7 @@ export function createInstancePeerStore(options?: {
|
|
|
149
187
|
return runMutation(async () => {
|
|
150
188
|
const table = await load();
|
|
151
189
|
const existing = table.instances[payload.instanceId];
|
|
190
|
+
const userPublicAttributes = normalizeUserPublicAttributes(payload.userPublicAttributes);
|
|
152
191
|
const changed = !sameRecord(existing, payload);
|
|
153
192
|
const record: InstancePeerRecord = {
|
|
154
193
|
instanceId: payload.instanceId,
|
|
@@ -156,6 +195,7 @@ export function createInstancePeerStore(options?: {
|
|
|
156
195
|
instanceName: payload.instanceName,
|
|
157
196
|
pubkey: payload.pubkey,
|
|
158
197
|
multiaddrs: payload.multiaddrs,
|
|
198
|
+
userPublicAttributes,
|
|
159
199
|
lastAnnouncedAt: payload.announcedAt,
|
|
160
200
|
lastSeenAt: Date.now(),
|
|
161
201
|
source: "announce",
|
package/src/instance-router.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
DeliveryAckPayload,
|
|
3
3
|
DeliveryTargetResult,
|
|
4
|
-
InboundDeliveryAdapter,
|
|
5
4
|
InboundTargetConfig,
|
|
6
5
|
InstanceAnnouncePayload,
|
|
7
|
-
InstancePeerStore,
|
|
8
6
|
InstanceRouter,
|
|
7
|
+
InstanceRouterOptions,
|
|
9
8
|
MeshConfig,
|
|
10
|
-
MeshNetwork,
|
|
11
9
|
P2PMessage,
|
|
10
|
+
UserAttributeMatch,
|
|
11
|
+
UserAttributeMessageTarget,
|
|
12
|
+
UserPublicAttribute,
|
|
12
13
|
UserMessagePayload,
|
|
13
14
|
} from "./types.js";
|
|
15
|
+
import { matchesUserAttribute, mergeUserPublicAttributes } from "./user-attributes.js";
|
|
14
16
|
|
|
15
17
|
export type RouterLogger = {
|
|
16
18
|
info?: (message: string) => void;
|
|
@@ -48,6 +50,14 @@ function summarizeError(error: unknown): string {
|
|
|
48
50
|
return error instanceof Error ? error.message : String(error);
|
|
49
51
|
}
|
|
50
52
|
|
|
53
|
+
function describeAttributeMatch(match: UserAttributeMatch): string {
|
|
54
|
+
if (match.kind === "tag") {
|
|
55
|
+
return `tag ${match.value}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return `structured attribute ${match.key}=${match.value}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
type EffectiveInboundTarget = {
|
|
52
62
|
id?: string;
|
|
53
63
|
channel: string;
|
|
@@ -135,13 +145,7 @@ function firstAttemptedResult(
|
|
|
135
145
|
);
|
|
136
146
|
}
|
|
137
147
|
|
|
138
|
-
export function createInstanceRouter(options: {
|
|
139
|
-
mesh: MeshNetwork;
|
|
140
|
-
store: InstancePeerStore;
|
|
141
|
-
delivery: InboundDeliveryAdapter;
|
|
142
|
-
config?: MeshConfig;
|
|
143
|
-
logger?: RouterLogger;
|
|
144
|
-
}): InstanceRouter {
|
|
148
|
+
export function createInstanceRouter(options: InstanceRouterOptions): InstanceRouter {
|
|
145
149
|
const { mesh, store, delivery } = options;
|
|
146
150
|
const config = options.config ?? {};
|
|
147
151
|
const logger = options.logger;
|
|
@@ -159,7 +163,26 @@ export function createInstanceRouter(options: {
|
|
|
159
163
|
return identity.id;
|
|
160
164
|
}
|
|
161
165
|
|
|
162
|
-
function
|
|
166
|
+
async function loadUserPublicAttributes(): Promise<UserPublicAttribute[]> {
|
|
167
|
+
const [userMdTags, profileAttributes] = await Promise.all([
|
|
168
|
+
options.userAttributeSource?.loadTags().catch((error) => {
|
|
169
|
+
logger?.warn?.(
|
|
170
|
+
`[libp2p-mesh] Failed to load USER.md public attributes: ${summarizeError(error)}`,
|
|
171
|
+
);
|
|
172
|
+
return [];
|
|
173
|
+
}) ?? Promise.resolve([]),
|
|
174
|
+
options.userProfileStore?.listAttributes().catch((error) => {
|
|
175
|
+
logger?.warn?.(
|
|
176
|
+
`[libp2p-mesh] Failed to load profile public attributes: ${summarizeError(error)}`,
|
|
177
|
+
);
|
|
178
|
+
return [];
|
|
179
|
+
}) ?? Promise.resolve([]),
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
return mergeUserPublicAttributes(userMdTags, profileAttributes);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function buildAnnouncePayload(): Promise<InstanceAnnouncePayload> {
|
|
163
186
|
const identity = mesh.getInstanceIdentity();
|
|
164
187
|
if (!identity) {
|
|
165
188
|
throw new Error("Local instance identity is not initialized");
|
|
@@ -171,6 +194,7 @@ export function createInstanceRouter(options: {
|
|
|
171
194
|
instanceName: identity.name,
|
|
172
195
|
multiaddrs: mesh.getMultiaddrs(),
|
|
173
196
|
pubkey: identity.pubkey,
|
|
197
|
+
userPublicAttributes: await loadUserPublicAttributes(),
|
|
174
198
|
announcedAt: Date.now(),
|
|
175
199
|
};
|
|
176
200
|
}
|
|
@@ -180,7 +204,7 @@ export function createInstanceRouter(options: {
|
|
|
180
204
|
return;
|
|
181
205
|
}
|
|
182
206
|
|
|
183
|
-
const payload = buildAnnouncePayload();
|
|
207
|
+
const payload = await buildAnnouncePayload();
|
|
184
208
|
await mesh.sendStructuredMessage(peerId, {
|
|
185
209
|
id: crypto.randomUUID(),
|
|
186
210
|
type: "instance-announce",
|
|
@@ -549,6 +573,90 @@ export function createInstanceRouter(options: {
|
|
|
549
573
|
};
|
|
550
574
|
}
|
|
551
575
|
|
|
576
|
+
async function resolveUserAttributeTargets(
|
|
577
|
+
match: UserAttributeMatch,
|
|
578
|
+
): Promise<UserAttributeMessageTarget[]> {
|
|
579
|
+
const records = await store.list();
|
|
580
|
+
const targets: UserAttributeMessageTarget[] = [];
|
|
581
|
+
|
|
582
|
+
for (const record of records) {
|
|
583
|
+
const matchedAttribute = record.userPublicAttributes?.find((attribute) =>
|
|
584
|
+
matchesUserAttribute(attribute, match),
|
|
585
|
+
);
|
|
586
|
+
if (!matchedAttribute) {
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
targets.push({
|
|
591
|
+
instanceId: record.instanceId,
|
|
592
|
+
instanceName: record.instanceName,
|
|
593
|
+
peerId: record.peerId,
|
|
594
|
+
matchedAttribute,
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return targets;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function sendUserAttributeMessage(
|
|
602
|
+
match: UserAttributeMatch,
|
|
603
|
+
message: string,
|
|
604
|
+
sendOptions: { dryRun?: boolean } = {},
|
|
605
|
+
) {
|
|
606
|
+
const targets = await resolveUserAttributeTargets(match);
|
|
607
|
+
if (targets.length === 0) {
|
|
608
|
+
return {
|
|
609
|
+
matched: 0,
|
|
610
|
+
sent: 0,
|
|
611
|
+
delivered: 0,
|
|
612
|
+
failed: 0,
|
|
613
|
+
error: `No discovered instances match ${describeAttributeMatch(match)}.`,
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (sendOptions.dryRun === true) {
|
|
618
|
+
return {
|
|
619
|
+
matched: targets.length,
|
|
620
|
+
sent: 0,
|
|
621
|
+
delivered: 0,
|
|
622
|
+
failed: 0,
|
|
623
|
+
targets,
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const results = [];
|
|
628
|
+
for (const target of targets) {
|
|
629
|
+
try {
|
|
630
|
+
const result = await sendInstanceMessage(target.instanceId, message);
|
|
631
|
+
const targetResult = {
|
|
632
|
+
...target,
|
|
633
|
+
sent: result.sent,
|
|
634
|
+
delivered: result.delivered,
|
|
635
|
+
};
|
|
636
|
+
if (result.error) {
|
|
637
|
+
results.push({ ...targetResult, error: result.error });
|
|
638
|
+
} else {
|
|
639
|
+
results.push(targetResult);
|
|
640
|
+
}
|
|
641
|
+
} catch (error) {
|
|
642
|
+
results.push({
|
|
643
|
+
...target,
|
|
644
|
+
sent: false,
|
|
645
|
+
delivered: false,
|
|
646
|
+
error: summarizeError(error),
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return {
|
|
652
|
+
matched: targets.length,
|
|
653
|
+
sent: results.filter((result) => result.sent).length,
|
|
654
|
+
delivered: results.filter((result) => result.delivered).length,
|
|
655
|
+
failed: results.filter((result) => !result.delivered).length,
|
|
656
|
+
results,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
552
660
|
return {
|
|
553
661
|
start,
|
|
554
662
|
stop,
|
|
@@ -557,5 +665,6 @@ export function createInstanceRouter(options: {
|
|
|
557
665
|
listInstances,
|
|
558
666
|
resolveInstance,
|
|
559
667
|
sendInstanceMessage,
|
|
668
|
+
sendUserAttributeMessage,
|
|
560
669
|
};
|
|
561
670
|
}
|
package/src/plugin.ts
CHANGED
|
@@ -5,13 +5,15 @@ import { createOpenClawRuntimeInboundDelivery } from "./inbound-delivery.js";
|
|
|
5
5
|
import { createInstancePeerStore } from "./instance-peer-store.js";
|
|
6
6
|
import { createInstanceRouter } from "./instance-router.js";
|
|
7
7
|
import { createMeshNetwork } from "./mesh.js";
|
|
8
|
+
import { createUserMdAttributeSource } from "./user-md-attributes.js";
|
|
9
|
+
import { createUserProfileStore } from "./user-profile-store.js";
|
|
8
10
|
import { buildP2PTools } from "./agent-tools.js";
|
|
9
|
-
import {
|
|
11
|
+
import { registerLibp2pMeshCli } from "./profile-cli.js";
|
|
10
12
|
import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
|
|
11
13
|
import type { MeshConfig } from "./types.js";
|
|
12
14
|
|
|
13
15
|
export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
14
|
-
|
|
16
|
+
registerLibp2pMeshCli(api);
|
|
15
17
|
|
|
16
18
|
const config = api.pluginConfig as MeshConfig | undefined;
|
|
17
19
|
let unsubscribeInbound: (() => void) | undefined;
|
|
@@ -21,6 +23,8 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
|
21
23
|
logger: api.logger,
|
|
22
24
|
});
|
|
23
25
|
const store = createInstancePeerStore({ logger: api.logger });
|
|
26
|
+
const userAttributeSource = createUserMdAttributeSource({ logger: api.logger });
|
|
27
|
+
const userProfileStore = createUserProfileStore({ logger: api.logger });
|
|
24
28
|
const delivery = createOpenClawRuntimeInboundDelivery({
|
|
25
29
|
config: api.config,
|
|
26
30
|
loadAdapter: async (channelId) => {
|
|
@@ -41,6 +45,8 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
|
41
45
|
delivery,
|
|
42
46
|
config,
|
|
43
47
|
logger: api.logger,
|
|
48
|
+
userAttributeSource,
|
|
49
|
+
userProfileStore,
|
|
44
50
|
});
|
|
45
51
|
|
|
46
52
|
const channel = createLibp2pMeshChannel(mesh);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
2
|
+
import type { OpenClawPluginCliContext } from "openclaw/plugin-sdk/plugin-runtime";
|
|
3
|
+
import {
|
|
4
|
+
createReadlinePrompter,
|
|
5
|
+
LIBP2P_MESH_CLI_REGISTRATION,
|
|
6
|
+
registerLibp2pMeshSetupCommand,
|
|
7
|
+
type ClosableSetupPrompter,
|
|
8
|
+
type SetupCliDeps,
|
|
9
|
+
} from "./setup-cli.js";
|
|
10
|
+
import { runProfileWizard } from "./profile-wizard.js";
|
|
11
|
+
import { createUserMdAttributeSource } from "./user-md-attributes.js";
|
|
12
|
+
import { createUserProfileStore, type UserProfileStore } from "./user-profile-store.js";
|
|
13
|
+
import type { SetupPrompter } from "./setup-wizard.js";
|
|
14
|
+
|
|
15
|
+
type CliRootCommand = {
|
|
16
|
+
command(name: string): {
|
|
17
|
+
description(text: string): {
|
|
18
|
+
action(handler: () => Promise<void>): void;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type ProfileCliDeps = {
|
|
24
|
+
createPrompter?: (ctx: OpenClawPluginCliContext) => SetupPrompter;
|
|
25
|
+
createProfileStore?: (api: OpenClawPluginApi) => Pick<UserProfileStore, "listAttributes" | "replaceAttributes">;
|
|
26
|
+
createUserMdAttributeSource?: (api: OpenClawPluginApi) => { loadTags(): Promise<Awaited<ReturnType<UserProfileStore["listAttributes"]>>> };
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type Libp2pMeshCliDeps = {
|
|
30
|
+
setup?: SetupCliDeps;
|
|
31
|
+
profile?: ProfileCliDeps;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function registerLibp2pMeshCli(api: OpenClawPluginApi, deps: Libp2pMeshCliDeps = {}): void {
|
|
35
|
+
api.registerCli((ctx) => {
|
|
36
|
+
const root = ctx.program
|
|
37
|
+
.command("libp2p-mesh")
|
|
38
|
+
.description("Configure libp2p-mesh plugin.");
|
|
39
|
+
|
|
40
|
+
registerLibp2pMeshSetupCommand(root, api, ctx, deps.setup);
|
|
41
|
+
registerLibp2pMeshProfileCommand(root, api, ctx, deps.profile);
|
|
42
|
+
}, LIBP2P_MESH_CLI_REGISTRATION);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function registerLibp2pMeshProfileCli(api: OpenClawPluginApi, deps: ProfileCliDeps = {}): void {
|
|
46
|
+
api.registerCli((ctx) => {
|
|
47
|
+
const root = ctx.program
|
|
48
|
+
.command("libp2p-mesh")
|
|
49
|
+
.description("Configure libp2p-mesh plugin.");
|
|
50
|
+
|
|
51
|
+
registerLibp2pMeshProfileCommand(root, api, ctx, deps);
|
|
52
|
+
}, LIBP2P_MESH_CLI_REGISTRATION);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function registerLibp2pMeshProfileCommand(
|
|
56
|
+
root: CliRootCommand,
|
|
57
|
+
api: OpenClawPluginApi,
|
|
58
|
+
ctx: OpenClawPluginCliContext,
|
|
59
|
+
deps: ProfileCliDeps = {},
|
|
60
|
+
): void {
|
|
61
|
+
root
|
|
62
|
+
.command("profile")
|
|
63
|
+
.description("Manage libp2p-mesh public profile attributes.")
|
|
64
|
+
.action(async () => {
|
|
65
|
+
const prompter = (deps.createPrompter?.(ctx) ?? createReadlinePrompter()) as ClosableSetupPrompter;
|
|
66
|
+
const profileStore = deps.createProfileStore?.(api) ?? createUserProfileStore({ logger: api.logger });
|
|
67
|
+
const userMdAttributeSource = deps.createUserMdAttributeSource?.(api) ?? createUserMdAttributeSource({ logger: api.logger });
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const result = await runProfileWizard({
|
|
71
|
+
prompter,
|
|
72
|
+
readOnlyTags: await userMdAttributeSource.loadTags(),
|
|
73
|
+
profileAttributes: await profileStore.listAttributes(),
|
|
74
|
+
writer: {
|
|
75
|
+
async replaceAttributes(attributes) {
|
|
76
|
+
await profileStore.replaceAttributes(attributes);
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
prompter.print(result.message);
|
|
81
|
+
} finally {
|
|
82
|
+
prompter.close?.();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|