libp2p-mesh 2026.5.17 → 2026.5.19
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/dist/src/agent-tools.d.ts +89 -13
- package/dist/src/agent-tools.js +107 -14
- package/dist/src/identity-exchange.d.ts +2 -1
- package/dist/src/identity-exchange.js +5 -4
- package/dist/src/inbound.js +2 -1
- package/dist/src/mesh.js +22 -0
- package/dist/src/peer-identity.d.ts +6 -0
- package/dist/src/peer-identity.js +14 -0
- package/dist/src/plugin.js +21 -7
- package/dist/src/types.d.ts +2 -0
- package/openclaw.plugin.json +1 -0
- package/package.json +1 -1
- package/src/agent-tools.ts +107 -15
- package/src/identity-exchange.ts +7 -3
- package/src/inbound.ts +3 -2
- package/src/mesh.ts +16 -0
- package/src/peer-identity.ts +18 -0
- package/src/plugin.ts +30 -6
- package/src/types.ts +2 -0
|
@@ -14,6 +14,7 @@ export declare function buildP2PTools(mesh: MeshNetwork): ({
|
|
|
14
14
|
type: "string";
|
|
15
15
|
description: string;
|
|
16
16
|
};
|
|
17
|
+
instanceId?: undefined;
|
|
17
18
|
topic?: undefined;
|
|
18
19
|
};
|
|
19
20
|
required: string[];
|
|
@@ -44,6 +45,78 @@ export declare function buildP2PTools(mesh: MeshNetwork): ({
|
|
|
44
45
|
};
|
|
45
46
|
isError: boolean;
|
|
46
47
|
}>;
|
|
48
|
+
} | {
|
|
49
|
+
name: string;
|
|
50
|
+
label: string;
|
|
51
|
+
description: string;
|
|
52
|
+
parameters: {
|
|
53
|
+
type: "object";
|
|
54
|
+
properties: {
|
|
55
|
+
instanceId: {
|
|
56
|
+
type: "string";
|
|
57
|
+
description: string;
|
|
58
|
+
};
|
|
59
|
+
message: {
|
|
60
|
+
type: "string";
|
|
61
|
+
description: string;
|
|
62
|
+
};
|
|
63
|
+
peerId?: undefined;
|
|
64
|
+
topic?: undefined;
|
|
65
|
+
};
|
|
66
|
+
required: string[];
|
|
67
|
+
};
|
|
68
|
+
execute(_toolCallId: string, params: {
|
|
69
|
+
instanceId: string;
|
|
70
|
+
message: string;
|
|
71
|
+
}, _ctx: any): Promise<{
|
|
72
|
+
content: {
|
|
73
|
+
type: "text";
|
|
74
|
+
text: string;
|
|
75
|
+
}[];
|
|
76
|
+
details: {
|
|
77
|
+
sent: boolean;
|
|
78
|
+
error: string;
|
|
79
|
+
instanceId?: undefined;
|
|
80
|
+
peerId?: undefined;
|
|
81
|
+
};
|
|
82
|
+
isError: boolean;
|
|
83
|
+
} | {
|
|
84
|
+
content: {
|
|
85
|
+
type: "text";
|
|
86
|
+
text: string;
|
|
87
|
+
}[];
|
|
88
|
+
details: {
|
|
89
|
+
sent: boolean;
|
|
90
|
+
instanceId: string;
|
|
91
|
+
error?: undefined;
|
|
92
|
+
peerId?: undefined;
|
|
93
|
+
};
|
|
94
|
+
isError: boolean;
|
|
95
|
+
} | {
|
|
96
|
+
content: {
|
|
97
|
+
type: "text";
|
|
98
|
+
text: string;
|
|
99
|
+
}[];
|
|
100
|
+
details: {
|
|
101
|
+
sent: boolean;
|
|
102
|
+
instanceId: string;
|
|
103
|
+
peerId: any;
|
|
104
|
+
error?: undefined;
|
|
105
|
+
};
|
|
106
|
+
isError?: undefined;
|
|
107
|
+
} | {
|
|
108
|
+
content: {
|
|
109
|
+
type: "text";
|
|
110
|
+
text: string;
|
|
111
|
+
}[];
|
|
112
|
+
details: {
|
|
113
|
+
sent: boolean;
|
|
114
|
+
instanceId: string;
|
|
115
|
+
error: string;
|
|
116
|
+
peerId?: undefined;
|
|
117
|
+
};
|
|
118
|
+
isError: boolean;
|
|
119
|
+
}>;
|
|
47
120
|
} | {
|
|
48
121
|
name: string;
|
|
49
122
|
label: string;
|
|
@@ -60,6 +133,7 @@ export declare function buildP2PTools(mesh: MeshNetwork): ({
|
|
|
60
133
|
description: string;
|
|
61
134
|
};
|
|
62
135
|
peerId?: undefined;
|
|
136
|
+
instanceId?: undefined;
|
|
63
137
|
};
|
|
64
138
|
required: string[];
|
|
65
139
|
};
|
|
@@ -98,19 +172,21 @@ export declare function buildP2PTools(mesh: MeshNetwork): ({
|
|
|
98
172
|
properties: {
|
|
99
173
|
peerId?: undefined;
|
|
100
174
|
message?: undefined;
|
|
175
|
+
instanceId?: undefined;
|
|
101
176
|
topic?: undefined;
|
|
102
177
|
};
|
|
103
178
|
required?: undefined;
|
|
104
179
|
};
|
|
105
|
-
execute(_toolCallId: string): Promise<{
|
|
180
|
+
execute(_toolCallId: string, _params: any, ctx: any): Promise<{
|
|
106
181
|
content: {
|
|
107
182
|
type: "text";
|
|
108
183
|
text: string;
|
|
109
184
|
}[];
|
|
110
185
|
details: {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
186
|
+
peers: {
|
|
187
|
+
instanceId: any;
|
|
188
|
+
channel: any;
|
|
189
|
+
}[];
|
|
114
190
|
error?: undefined;
|
|
115
191
|
};
|
|
116
192
|
isError?: undefined;
|
|
@@ -121,9 +197,7 @@ export declare function buildP2PTools(mesh: MeshNetwork): ({
|
|
|
121
197
|
}[];
|
|
122
198
|
details: {
|
|
123
199
|
error: string;
|
|
124
|
-
|
|
125
|
-
connectedPeers?: undefined;
|
|
126
|
-
count?: undefined;
|
|
200
|
+
peers?: undefined;
|
|
127
201
|
};
|
|
128
202
|
isError: boolean;
|
|
129
203
|
}>;
|
|
@@ -136,6 +210,7 @@ export declare function buildP2PTools(mesh: MeshNetwork): ({
|
|
|
136
210
|
properties: {
|
|
137
211
|
peerId?: undefined;
|
|
138
212
|
message?: undefined;
|
|
213
|
+
instanceId?: undefined;
|
|
139
214
|
topic?: undefined;
|
|
140
215
|
};
|
|
141
216
|
required?: undefined;
|
|
@@ -183,6 +258,7 @@ export declare function buildP2PTools(mesh: MeshNetwork): ({
|
|
|
183
258
|
properties: {
|
|
184
259
|
peerId?: undefined;
|
|
185
260
|
message?: undefined;
|
|
261
|
+
instanceId?: undefined;
|
|
186
262
|
topic?: undefined;
|
|
187
263
|
};
|
|
188
264
|
required?: undefined;
|
|
@@ -223,6 +299,7 @@ export declare function buildP2PTools(mesh: MeshNetwork): ({
|
|
|
223
299
|
properties: {
|
|
224
300
|
peerId?: undefined;
|
|
225
301
|
message?: undefined;
|
|
302
|
+
instanceId?: undefined;
|
|
226
303
|
topic?: undefined;
|
|
227
304
|
};
|
|
228
305
|
required?: undefined;
|
|
@@ -233,12 +310,11 @@ export declare function buildP2PTools(mesh: MeshNetwork): ({
|
|
|
233
310
|
text: string;
|
|
234
311
|
}[];
|
|
235
312
|
details: {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}[];
|
|
313
|
+
knownIdentities: ({
|
|
314
|
+
instanceId: any;
|
|
315
|
+
agentId: any;
|
|
316
|
+
channel: any;
|
|
317
|
+
} | null)[];
|
|
242
318
|
};
|
|
243
319
|
}>;
|
|
244
320
|
})[];
|
package/dist/src/agent-tools.js
CHANGED
|
@@ -3,7 +3,7 @@ export function buildP2PTools(mesh) {
|
|
|
3
3
|
{
|
|
4
4
|
name: "p2p_send_message",
|
|
5
5
|
label: "P2P Send Message",
|
|
6
|
-
description: "Send a direct message to another agent via the P2P mesh network.",
|
|
6
|
+
description: "Send a direct message to another agent via the P2P mesh network by Peer ID.",
|
|
7
7
|
parameters: {
|
|
8
8
|
type: "object",
|
|
9
9
|
properties: {
|
|
@@ -40,6 +40,72 @@ export function buildP2PTools(mesh) {
|
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
},
|
|
43
|
+
{
|
|
44
|
+
name: "p2p_send_to_instance",
|
|
45
|
+
label: "P2P Send to Instance",
|
|
46
|
+
description: "Send a direct message to a peer by its human-readable Instance ID (e.g. alice-mac).",
|
|
47
|
+
parameters: {
|
|
48
|
+
type: "object",
|
|
49
|
+
properties: {
|
|
50
|
+
instanceId: {
|
|
51
|
+
type: "string",
|
|
52
|
+
description: "Target Instance ID (e.g. alice-mac@AQIDBAU.7a3f9e2b)",
|
|
53
|
+
},
|
|
54
|
+
message: {
|
|
55
|
+
type: "string",
|
|
56
|
+
description: "Message content to send",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
required: ["instanceId", "message"],
|
|
60
|
+
},
|
|
61
|
+
async execute(_toolCallId, params, _ctx) {
|
|
62
|
+
const peerIdentityMap = _ctx?.peerIdentityMap;
|
|
63
|
+
if (!peerIdentityMap) {
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: "text", text: "Internal error: peer identity map not available." }],
|
|
66
|
+
details: { sent: false, error: "peerIdentityMap missing" },
|
|
67
|
+
isError: true,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const lookup = peerIdentityMap.resolveByInstanceId(params.instanceId);
|
|
71
|
+
if (!lookup) {
|
|
72
|
+
return {
|
|
73
|
+
content: [
|
|
74
|
+
{
|
|
75
|
+
type: "text",
|
|
76
|
+
text: `Unknown Instance ID: "${params.instanceId}". Use p2p_list_peers to see available users.`,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
details: { sent: false, instanceId: params.instanceId },
|
|
80
|
+
isError: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
await mesh.sendToPeer(lookup.peerId, params.message);
|
|
85
|
+
return {
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: "text",
|
|
89
|
+
text: `Message sent to ${params.instanceId}`,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
details: { sent: true, instanceId: params.instanceId, peerId: lookup.peerId },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
return {
|
|
97
|
+
content: [
|
|
98
|
+
{
|
|
99
|
+
type: "text",
|
|
100
|
+
text: `Failed to send message to ${params.instanceId}: ${String(err)}`,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
details: { sent: false, instanceId: params.instanceId, error: String(err) },
|
|
104
|
+
isError: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
},
|
|
43
109
|
{
|
|
44
110
|
name: "p2p_broadcast",
|
|
45
111
|
label: "P2P Broadcast",
|
|
@@ -83,24 +149,42 @@ export function buildP2PTools(mesh) {
|
|
|
83
149
|
{
|
|
84
150
|
name: "p2p_list_peers",
|
|
85
151
|
label: "P2P List Peers",
|
|
86
|
-
description: "List currently connected peers
|
|
152
|
+
description: "List currently connected peers with their Instance IDs and channels.",
|
|
87
153
|
parameters: {
|
|
88
154
|
type: "object",
|
|
89
155
|
properties: {},
|
|
90
156
|
},
|
|
91
|
-
async execute(_toolCallId) {
|
|
157
|
+
async execute(_toolCallId, _params, ctx) {
|
|
92
158
|
try {
|
|
159
|
+
const peerIdentityMap = ctx?.peerIdentityMap;
|
|
93
160
|
const peers = mesh.getConnectedPeers();
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
161
|
+
if (peers.length === 0) {
|
|
162
|
+
return {
|
|
163
|
+
content: [{ type: "text", text: "No peers currently connected." }],
|
|
164
|
+
details: { peers: [] },
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
const userList = peers
|
|
168
|
+
.map((p) => {
|
|
169
|
+
const identity = peerIdentityMap?.resolve(p);
|
|
170
|
+
if (identity?.instanceId) {
|
|
171
|
+
const ch = identity.channel ? ` (${identity.channel})` : "";
|
|
172
|
+
return ` - ${identity.instanceId}${ch}`;
|
|
173
|
+
}
|
|
174
|
+
return ` - ${p.slice(0, 12)}... (identity unknown)`;
|
|
175
|
+
})
|
|
176
|
+
.join("\n");
|
|
177
|
+
const text = `在线用户 (${peers.length}):\n${userList}`;
|
|
178
|
+
const safePeers = peers.map((p) => {
|
|
179
|
+
const identity = peerIdentityMap?.resolve(p);
|
|
180
|
+
return {
|
|
181
|
+
instanceId: identity?.instanceId || p.slice(0, 12) + "...",
|
|
182
|
+
channel: identity?.channel || "unknown",
|
|
183
|
+
};
|
|
184
|
+
});
|
|
97
185
|
return {
|
|
98
186
|
content: [{ type: "text", text }],
|
|
99
|
-
details: {
|
|
100
|
-
localPeerId: mesh.getLocalPeerId(),
|
|
101
|
-
connectedPeers: peers,
|
|
102
|
-
count: peers.length,
|
|
103
|
-
},
|
|
187
|
+
details: { peers: safePeers },
|
|
104
188
|
};
|
|
105
189
|
}
|
|
106
190
|
catch (err) {
|
|
@@ -201,12 +285,21 @@ export function buildP2PTools(mesh) {
|
|
|
201
285
|
const localPeerId = mesh.getLocalPeerId();
|
|
202
286
|
const connectedPeers = mesh.getConnectedPeers();
|
|
203
287
|
const knownIdentities = connectedPeers
|
|
204
|
-
.map((p) =>
|
|
205
|
-
|
|
288
|
+
.map((p) => {
|
|
289
|
+
const identity = peerIdentityMap?.resolve(p);
|
|
290
|
+
if (!identity)
|
|
291
|
+
return null;
|
|
292
|
+
return {
|
|
293
|
+
instanceId: identity.instanceId || p.slice(0, 12) + "...",
|
|
294
|
+
agentId: identity.agentId,
|
|
295
|
+
channel: identity.channel,
|
|
296
|
+
};
|
|
297
|
+
})
|
|
298
|
+
.filter(Boolean);
|
|
206
299
|
const text = `P2P Mesh Status:\nLocal Peer ID: ${localPeerId}\nConnected Peers: ${connectedPeers.length}\nKnown Identities: ${knownIdentities.length}`;
|
|
207
300
|
return {
|
|
208
301
|
content: [{ type: "text", text }],
|
|
209
|
-
details: {
|
|
302
|
+
details: { knownIdentities },
|
|
210
303
|
};
|
|
211
304
|
},
|
|
212
305
|
},
|
|
@@ -6,7 +6,8 @@ export interface IdentityExchangeDeps {
|
|
|
6
6
|
localAgentId: string;
|
|
7
7
|
localChannel: string;
|
|
8
8
|
localAccountId: string;
|
|
9
|
+
localInstanceId?: string;
|
|
9
10
|
send: (peerId: string, message: P2PMessage) => Promise<void>;
|
|
10
11
|
}
|
|
11
|
-
export declare function buildIdentityMessage(peerId: string, agentId: string, channel: string, accountId: string): P2PMessage;
|
|
12
|
+
export declare function buildIdentityMessage(peerId: string, agentId: string, channel: string, accountId: string, instanceId?: string): P2PMessage;
|
|
12
13
|
export declare function handleIdentityMessage(msg: P2PMessage, deps: IdentityExchangeDeps): Promise<void>;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export function buildIdentityMessage(peerId, agentId, channel, accountId) {
|
|
1
|
+
export function buildIdentityMessage(peerId, agentId, channel, accountId, instanceId) {
|
|
2
2
|
return {
|
|
3
3
|
id: crypto.randomUUID(),
|
|
4
4
|
type: "identity",
|
|
5
5
|
from: peerId,
|
|
6
|
-
payload: JSON.stringify({ agentId, channel, accountId }),
|
|
6
|
+
payload: JSON.stringify({ agentId, channel, accountId, instanceId }),
|
|
7
7
|
timestamp: Date.now(),
|
|
8
8
|
};
|
|
9
9
|
}
|
|
@@ -17,7 +17,7 @@ export async function handleIdentityMessage(msg, deps) {
|
|
|
17
17
|
// Malformed payload — skip silently
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
-
const { agentId, channel, accountId } = parsed;
|
|
20
|
+
const { agentId, channel, accountId, instanceId } = parsed;
|
|
21
21
|
// Only register if all required fields are present
|
|
22
22
|
if (agentId && channel && accountId) {
|
|
23
23
|
const { buildAgentSessionKey } = await import("openclaw/plugin-sdk/core");
|
|
@@ -27,9 +27,10 @@ export async function handleIdentityMessage(msg, deps) {
|
|
|
27
27
|
channel,
|
|
28
28
|
accountId,
|
|
29
29
|
sessionKey,
|
|
30
|
+
instanceId,
|
|
30
31
|
});
|
|
31
32
|
}
|
|
32
33
|
// Send local identity back to the remote peer
|
|
33
|
-
const localIdentity = buildIdentityMessage(deps.localPeerId, deps.localAgentId, deps.localChannel, deps.localAccountId);
|
|
34
|
+
const localIdentity = buildIdentityMessage(deps.localPeerId, deps.localAgentId, deps.localChannel, deps.localAccountId, deps.localInstanceId);
|
|
34
35
|
await deps.send(msg.from, localIdentity);
|
|
35
36
|
}
|
package/dist/src/inbound.js
CHANGED
|
@@ -46,7 +46,7 @@ export function handleP2PInbound(msg, deps) {
|
|
|
46
46
|
// Malformed payload — skip silently
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
|
-
const { agentId, channel, accountId } = parsed;
|
|
49
|
+
const { agentId, channel, accountId, instanceId } = parsed;
|
|
50
50
|
if (agentId && channel && accountId) {
|
|
51
51
|
const sessionKey = buildAgentSessionKey({ agentId, channel, accountId });
|
|
52
52
|
deps.peerIdentityMap.register(msg.from, {
|
|
@@ -54,6 +54,7 @@ export function handleP2PInbound(msg, deps) {
|
|
|
54
54
|
channel,
|
|
55
55
|
accountId,
|
|
56
56
|
sessionKey,
|
|
57
|
+
instanceId,
|
|
57
58
|
});
|
|
58
59
|
}
|
|
59
60
|
}
|
package/dist/src/mesh.js
CHANGED
|
@@ -77,6 +77,8 @@ export function createMeshNetwork(options) {
|
|
|
77
77
|
const seenMessages = new Set();
|
|
78
78
|
const messageHandlers = new Set();
|
|
79
79
|
const topicHandlers = new Map();
|
|
80
|
+
const peerConnectHandlers = new Set();
|
|
81
|
+
const peerDisconnectHandlers = new Set();
|
|
80
82
|
function getDHTService() {
|
|
81
83
|
return state.node?.services?.dht;
|
|
82
84
|
}
|
|
@@ -215,10 +217,22 @@ export function createMeshNetwork(options) {
|
|
|
215
217
|
state.node.addEventListener("peer:connect", (evt) => {
|
|
216
218
|
const peerIdStr = evt.detail.toString();
|
|
217
219
|
logger?.info?.(`[libp2p-mesh] Peer connected: ${peerIdStr}`);
|
|
220
|
+
for (const handler of peerConnectHandlers) {
|
|
221
|
+
try {
|
|
222
|
+
handler(peerIdStr);
|
|
223
|
+
}
|
|
224
|
+
catch { }
|
|
225
|
+
}
|
|
218
226
|
});
|
|
219
227
|
state.node.addEventListener("peer:disconnect", (evt) => {
|
|
220
228
|
const peerIdStr = evt.detail.toString();
|
|
221
229
|
logger?.info?.(`[libp2p-mesh] Peer disconnected: ${peerIdStr}`);
|
|
230
|
+
for (const handler of peerDisconnectHandlers) {
|
|
231
|
+
try {
|
|
232
|
+
handler(peerIdStr);
|
|
233
|
+
}
|
|
234
|
+
catch { }
|
|
235
|
+
}
|
|
222
236
|
});
|
|
223
237
|
await state.node.handle(PROTOCOL, async ({ stream, connection }) => {
|
|
224
238
|
try {
|
|
@@ -580,6 +594,14 @@ export function createMeshNetwork(options) {
|
|
|
580
594
|
stop,
|
|
581
595
|
sendToPeer,
|
|
582
596
|
onMessage,
|
|
597
|
+
onPeerConnect(handler) {
|
|
598
|
+
peerConnectHandlers.add(handler);
|
|
599
|
+
return () => { peerConnectHandlers.delete(handler); };
|
|
600
|
+
},
|
|
601
|
+
onPeerDisconnect(handler) {
|
|
602
|
+
peerDisconnectHandlers.add(handler);
|
|
603
|
+
return () => { peerDisconnectHandlers.delete(handler); };
|
|
604
|
+
},
|
|
583
605
|
publishToTopic,
|
|
584
606
|
subscribeToTopic,
|
|
585
607
|
getLocalPeerId,
|
|
@@ -3,11 +3,17 @@ export interface PeerIdentity {
|
|
|
3
3
|
channel: string;
|
|
4
4
|
accountId: string;
|
|
5
5
|
sessionKey: string;
|
|
6
|
+
/** Human-readable Instance ID (e.g. "alice-mac@AQIDBAU.7a3f9e2b") */
|
|
7
|
+
instanceId?: string;
|
|
6
8
|
}
|
|
7
9
|
export interface PeerIdentityMap {
|
|
8
10
|
register(peerId: string, identity: PeerIdentity): void;
|
|
9
11
|
resolve(peerId: string): PeerIdentity | undefined;
|
|
10
12
|
resolveSessionKey(peerId: string): string | undefined;
|
|
13
|
+
resolveByInstanceId(instanceId: string): {
|
|
14
|
+
peerId: string;
|
|
15
|
+
identity: PeerIdentity;
|
|
16
|
+
} | undefined;
|
|
11
17
|
unregister(peerId: string): void;
|
|
12
18
|
setLocalIdentity(peerId: string, identity: PeerIdentity): void;
|
|
13
19
|
getLocalIdentity(): PeerIdentity | undefined;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
export function createPeerIdentityMap() {
|
|
2
2
|
const peers = new Map();
|
|
3
|
+
const byInstanceId = new Map();
|
|
3
4
|
let localPeerId;
|
|
4
5
|
let localIdentity;
|
|
5
6
|
return {
|
|
6
7
|
register(peerId, identity) {
|
|
7
8
|
peers.set(peerId, identity);
|
|
9
|
+
if (identity.instanceId) {
|
|
10
|
+
byInstanceId.set(identity.instanceId, { peerId, identity });
|
|
11
|
+
}
|
|
8
12
|
},
|
|
9
13
|
resolve(peerId) {
|
|
10
14
|
return peers.get(peerId);
|
|
@@ -12,13 +16,23 @@ export function createPeerIdentityMap() {
|
|
|
12
16
|
resolveSessionKey(peerId) {
|
|
13
17
|
return peers.get(peerId)?.sessionKey;
|
|
14
18
|
},
|
|
19
|
+
resolveByInstanceId(instanceId) {
|
|
20
|
+
return byInstanceId.get(instanceId);
|
|
21
|
+
},
|
|
15
22
|
unregister(peerId) {
|
|
23
|
+
const identity = peers.get(peerId);
|
|
24
|
+
if (identity?.instanceId) {
|
|
25
|
+
byInstanceId.delete(identity.instanceId);
|
|
26
|
+
}
|
|
16
27
|
peers.delete(peerId);
|
|
17
28
|
},
|
|
18
29
|
setLocalIdentity(peerId, identity) {
|
|
19
30
|
localPeerId = peerId;
|
|
20
31
|
localIdentity = identity;
|
|
21
32
|
peers.set(peerId, identity);
|
|
33
|
+
if (identity.instanceId) {
|
|
34
|
+
byInstanceId.set(identity.instanceId, { peerId, identity });
|
|
35
|
+
}
|
|
22
36
|
},
|
|
23
37
|
getLocalIdentity() {
|
|
24
38
|
return localIdentity;
|
package/dist/src/plugin.js
CHANGED
|
@@ -24,12 +24,15 @@ export function registerLibp2pMesh(api) {
|
|
|
24
24
|
id: "libp2p-mesh",
|
|
25
25
|
start: async () => {
|
|
26
26
|
await mesh.start();
|
|
27
|
-
//
|
|
27
|
+
// Gather local relay identity info
|
|
28
28
|
const config = api.pluginConfig;
|
|
29
29
|
const relayChannel = config?.relayChannel;
|
|
30
30
|
const relayAccountId = config?.relayAccountId;
|
|
31
|
+
const instanceIdentity = mesh.getInstanceIdentity();
|
|
32
|
+
const localInstanceId = instanceIdentity?.id;
|
|
33
|
+
const localPeerId = mesh.getLocalPeerId();
|
|
34
|
+
// Register local identity so remote peers can route messages back to us
|
|
31
35
|
if (relayChannel && relayAccountId) {
|
|
32
|
-
const localPeerId = mesh.getLocalPeerId();
|
|
33
36
|
const { buildAgentSessionKey } = await import("openclaw/plugin-sdk/core");
|
|
34
37
|
const sessionKey = buildAgentSessionKey({
|
|
35
38
|
agentId: api.name,
|
|
@@ -41,12 +44,12 @@ export function registerLibp2pMesh(api) {
|
|
|
41
44
|
channel: relayChannel,
|
|
42
45
|
accountId: relayAccountId,
|
|
43
46
|
sessionKey,
|
|
47
|
+
instanceId: localInstanceId,
|
|
44
48
|
});
|
|
45
|
-
api.logger.info?.(`[libp2p-mesh] Local identity registered: agent=${api.name}, channel=${relayChannel}, account=${relayAccountId}`);
|
|
49
|
+
api.logger.info?.(`[libp2p-mesh] Local identity registered: agent=${api.name}, channel=${relayChannel}, account=${relayAccountId}, instanceId=${localInstanceId}`);
|
|
46
50
|
// Announce identity to all currently-connected peers
|
|
47
|
-
const identityMsg = buildIdentityMessage(localPeerId, api.name, relayChannel, relayAccountId);
|
|
48
|
-
const
|
|
49
|
-
for (const peerId of connectedPeers) {
|
|
51
|
+
const identityMsg = buildIdentityMessage(localPeerId, api.name, relayChannel, relayAccountId, localInstanceId);
|
|
52
|
+
for (const peerId of mesh.getConnectedPeers()) {
|
|
50
53
|
try {
|
|
51
54
|
await mesh.sendToPeer(peerId, JSON.stringify(identityMsg));
|
|
52
55
|
}
|
|
@@ -54,13 +57,24 @@ export function registerLibp2pMesh(api) {
|
|
|
54
57
|
// Best-effort; peer may be stale in the connection list
|
|
55
58
|
}
|
|
56
59
|
}
|
|
60
|
+
// When a new peer connects, send our identity to them
|
|
61
|
+
mesh.onPeerConnect((peerId) => {
|
|
62
|
+
const msg = buildIdentityMessage(localPeerId, api.name, relayChannel, relayAccountId, localInstanceId);
|
|
63
|
+
mesh.sendToPeer(peerId, JSON.stringify(msg)).catch(() => {
|
|
64
|
+
// Best-effort
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
// When a peer disconnects, clean up the identity map
|
|
68
|
+
mesh.onPeerDisconnect((peerId) => {
|
|
69
|
+
peerIdentityMap.unregister(peerId);
|
|
70
|
+
});
|
|
57
71
|
}
|
|
58
72
|
// Wire up relay-aware message handler
|
|
59
73
|
mesh.onMessage((msg) => {
|
|
60
74
|
handleP2PInbound(msg, buildInboundDeps());
|
|
61
75
|
});
|
|
62
76
|
const identity = mesh.getInstanceIdentity();
|
|
63
|
-
api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${
|
|
77
|
+
api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${localPeerId}`);
|
|
64
78
|
if (identity) {
|
|
65
79
|
api.logger.info?.(`[libp2p-mesh] Instance Identity: ${identity.id}`);
|
|
66
80
|
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -106,6 +106,8 @@ export interface MeshNetwork {
|
|
|
106
106
|
stop(): Promise<void>;
|
|
107
107
|
sendToPeer(peerId: string, message: string): Promise<void>;
|
|
108
108
|
onMessage(handler: (msg: P2PMessage) => void): () => void;
|
|
109
|
+
onPeerConnect(handler: (peerId: string) => void): () => void;
|
|
110
|
+
onPeerDisconnect(handler: (peerId: string) => void): () => void;
|
|
109
111
|
publishToTopic(topic: string, message: string): Promise<void>;
|
|
110
112
|
subscribeToTopic(topic: string, handler: (msg: string) => void): Promise<void>;
|
|
111
113
|
getLocalPeerId(): string;
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/agent-tools.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { MeshNetwork } from "./types.js";
|
|
2
|
+
import type { PeerIdentity } from "./peer-identity.ts";
|
|
3
|
+
import type { PeerIdentityMap } from "./peer-identity.ts";
|
|
2
4
|
|
|
3
5
|
export function buildP2PTools(mesh: MeshNetwork) {
|
|
4
6
|
return [
|
|
5
7
|
{
|
|
6
8
|
name: "p2p_send_message",
|
|
7
9
|
label: "P2P Send Message",
|
|
8
|
-
description: "Send a direct message to another agent via the P2P mesh network.",
|
|
10
|
+
description: "Send a direct message to another agent via the P2P mesh network by Peer ID.",
|
|
9
11
|
parameters: {
|
|
10
12
|
type: "object" as const,
|
|
11
13
|
properties: {
|
|
@@ -41,6 +43,71 @@ export function buildP2PTools(mesh: MeshNetwork) {
|
|
|
41
43
|
}
|
|
42
44
|
},
|
|
43
45
|
},
|
|
46
|
+
{
|
|
47
|
+
name: "p2p_send_to_instance",
|
|
48
|
+
label: "P2P Send to Instance",
|
|
49
|
+
description: "Send a direct message to a peer by its human-readable Instance ID (e.g. alice-mac).",
|
|
50
|
+
parameters: {
|
|
51
|
+
type: "object" as const,
|
|
52
|
+
properties: {
|
|
53
|
+
instanceId: {
|
|
54
|
+
type: "string" as const,
|
|
55
|
+
description: "Target Instance ID (e.g. alice-mac@AQIDBAU.7a3f9e2b)",
|
|
56
|
+
},
|
|
57
|
+
message: {
|
|
58
|
+
type: "string" as const,
|
|
59
|
+
description: "Message content to send",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
required: ["instanceId", "message"],
|
|
63
|
+
},
|
|
64
|
+
async execute(_toolCallId: string, params: { instanceId: string; message: string }, _ctx: any) {
|
|
65
|
+
const peerIdentityMap = (_ctx as any)?.peerIdentityMap;
|
|
66
|
+
if (!peerIdentityMap) {
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: "text" as const, text: "Internal error: peer identity map not available." }],
|
|
69
|
+
details: { sent: false, error: "peerIdentityMap missing" },
|
|
70
|
+
isError: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const lookup = peerIdentityMap.resolveByInstanceId(params.instanceId);
|
|
74
|
+
if (!lookup) {
|
|
75
|
+
return {
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: "text" as const,
|
|
79
|
+
text: `Unknown Instance ID: "${params.instanceId}". Use p2p_list_peers to see available users.`,
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
details: { sent: false, instanceId: params.instanceId },
|
|
83
|
+
isError: true,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
await mesh.sendToPeer(lookup.peerId, params.message);
|
|
88
|
+
return {
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: "text" as const,
|
|
92
|
+
text: `Message sent to ${params.instanceId}`,
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
details: { sent: true, instanceId: params.instanceId, peerId: lookup.peerId },
|
|
96
|
+
};
|
|
97
|
+
} catch (err) {
|
|
98
|
+
return {
|
|
99
|
+
content: [
|
|
100
|
+
{
|
|
101
|
+
type: "text" as const,
|
|
102
|
+
text: `Failed to send message to ${params.instanceId}: ${String(err)}`,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
details: { sent: false, instanceId: params.instanceId, error: String(err) },
|
|
106
|
+
isError: true,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
},
|
|
44
111
|
{
|
|
45
112
|
name: "p2p_broadcast",
|
|
46
113
|
label: "P2P Broadcast",
|
|
@@ -83,25 +150,42 @@ export function buildP2PTools(mesh: MeshNetwork) {
|
|
|
83
150
|
{
|
|
84
151
|
name: "p2p_list_peers",
|
|
85
152
|
label: "P2P List Peers",
|
|
86
|
-
description: "List currently connected peers
|
|
153
|
+
description: "List currently connected peers with their Instance IDs and channels.",
|
|
87
154
|
parameters: {
|
|
88
155
|
type: "object" as const,
|
|
89
156
|
properties: {},
|
|
90
157
|
},
|
|
91
|
-
async execute(_toolCallId: string) {
|
|
158
|
+
async execute(_toolCallId: string, _params: any, ctx: any) {
|
|
92
159
|
try {
|
|
160
|
+
const peerIdentityMap = ctx?.peerIdentityMap;
|
|
93
161
|
const peers = mesh.getConnectedPeers();
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
:
|
|
162
|
+
if (peers.length === 0) {
|
|
163
|
+
return {
|
|
164
|
+
content: [{ type: "text" as const, text: "No peers currently connected." }],
|
|
165
|
+
details: { peers: [] },
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const userList = peers
|
|
169
|
+
.map((p) => {
|
|
170
|
+
const identity = peerIdentityMap?.resolve(p);
|
|
171
|
+
if (identity?.instanceId) {
|
|
172
|
+
const ch = identity.channel ? ` (${identity.channel})` : "";
|
|
173
|
+
return ` - ${identity.instanceId}${ch}`;
|
|
174
|
+
}
|
|
175
|
+
return ` - ${p.slice(0, 12)}... (identity unknown)`;
|
|
176
|
+
})
|
|
177
|
+
.join("\n");
|
|
178
|
+
const text = `在线用户 (${peers.length}):\n${userList}`;
|
|
179
|
+
const safePeers = peers.map((p) => {
|
|
180
|
+
const identity = peerIdentityMap?.resolve(p);
|
|
181
|
+
return {
|
|
182
|
+
instanceId: identity?.instanceId || p.slice(0, 12) + "...",
|
|
183
|
+
channel: identity?.channel || "unknown",
|
|
184
|
+
};
|
|
185
|
+
});
|
|
98
186
|
return {
|
|
99
187
|
content: [{ type: "text" as const, text }],
|
|
100
|
-
details: {
|
|
101
|
-
localPeerId: mesh.getLocalPeerId(),
|
|
102
|
-
connectedPeers: peers,
|
|
103
|
-
count: peers.length,
|
|
104
|
-
},
|
|
188
|
+
details: { peers: safePeers },
|
|
105
189
|
};
|
|
106
190
|
} catch (err) {
|
|
107
191
|
return {
|
|
@@ -201,12 +285,20 @@ export function buildP2PTools(mesh: MeshNetwork) {
|
|
|
201
285
|
const localPeerId = mesh.getLocalPeerId();
|
|
202
286
|
const connectedPeers = mesh.getConnectedPeers();
|
|
203
287
|
const knownIdentities = connectedPeers
|
|
204
|
-
.map((p) =>
|
|
205
|
-
|
|
288
|
+
.map((p) => {
|
|
289
|
+
const identity = peerIdentityMap?.resolve(p);
|
|
290
|
+
if (!identity) return null;
|
|
291
|
+
return {
|
|
292
|
+
instanceId: identity.instanceId || p.slice(0, 12) + "...",
|
|
293
|
+
agentId: identity.agentId,
|
|
294
|
+
channel: identity.channel,
|
|
295
|
+
};
|
|
296
|
+
})
|
|
297
|
+
.filter(Boolean);
|
|
206
298
|
const text = `P2P Mesh Status:\nLocal Peer ID: ${localPeerId}\nConnected Peers: ${connectedPeers.length}\nKnown Identities: ${knownIdentities.length}`;
|
|
207
299
|
return {
|
|
208
300
|
content: [{ type: "text" as const, text }],
|
|
209
|
-
details: {
|
|
301
|
+
details: { knownIdentities },
|
|
210
302
|
};
|
|
211
303
|
},
|
|
212
304
|
},
|
package/src/identity-exchange.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface IdentityExchangeDeps {
|
|
|
7
7
|
localAgentId: string;
|
|
8
8
|
localChannel: string;
|
|
9
9
|
localAccountId: string;
|
|
10
|
+
localInstanceId?: string;
|
|
10
11
|
send: (peerId: string, message: P2PMessage) => Promise<void>;
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -15,12 +16,13 @@ export function buildIdentityMessage(
|
|
|
15
16
|
agentId: string,
|
|
16
17
|
channel: string,
|
|
17
18
|
accountId: string,
|
|
19
|
+
instanceId?: string,
|
|
18
20
|
): P2PMessage {
|
|
19
21
|
return {
|
|
20
22
|
id: crypto.randomUUID(),
|
|
21
23
|
type: "identity",
|
|
22
24
|
from: peerId,
|
|
23
|
-
payload: JSON.stringify({ agentId, channel, accountId }),
|
|
25
|
+
payload: JSON.stringify({ agentId, channel, accountId, instanceId }),
|
|
24
26
|
timestamp: Date.now(),
|
|
25
27
|
};
|
|
26
28
|
}
|
|
@@ -30,7 +32,7 @@ export async function handleIdentityMessage(
|
|
|
30
32
|
deps: IdentityExchangeDeps,
|
|
31
33
|
): Promise<void> {
|
|
32
34
|
// Parse remote identity payload
|
|
33
|
-
let parsed: { agentId?: string; channel?: string; accountId?: string } = {};
|
|
35
|
+
let parsed: { agentId?: string; channel?: string; accountId?: string; instanceId?: string } = {};
|
|
34
36
|
try {
|
|
35
37
|
parsed = JSON.parse(msg.payload);
|
|
36
38
|
} catch {
|
|
@@ -38,7 +40,7 @@ export async function handleIdentityMessage(
|
|
|
38
40
|
return;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
const { agentId, channel, accountId } = parsed;
|
|
43
|
+
const { agentId, channel, accountId, instanceId } = parsed;
|
|
42
44
|
|
|
43
45
|
// Only register if all required fields are present
|
|
44
46
|
if (agentId && channel && accountId) {
|
|
@@ -49,6 +51,7 @@ export async function handleIdentityMessage(
|
|
|
49
51
|
channel,
|
|
50
52
|
accountId,
|
|
51
53
|
sessionKey,
|
|
54
|
+
instanceId,
|
|
52
55
|
});
|
|
53
56
|
}
|
|
54
57
|
|
|
@@ -58,6 +61,7 @@ export async function handleIdentityMessage(
|
|
|
58
61
|
deps.localAgentId,
|
|
59
62
|
deps.localChannel,
|
|
60
63
|
deps.localAccountId,
|
|
64
|
+
deps.localInstanceId,
|
|
61
65
|
);
|
|
62
66
|
await deps.send(msg.from, localIdentity);
|
|
63
67
|
}
|
package/src/inbound.ts
CHANGED
|
@@ -57,7 +57,7 @@ export function handleP2PInbound(msg: P2PMessage, deps?: InboundHandlerDeps): vo
|
|
|
57
57
|
if (deps?.peerIdentityMap && deps?.enqueueNextTurnInjection) {
|
|
58
58
|
if (msg.type === "identity") {
|
|
59
59
|
// Parse remote identity payload and register synchronously
|
|
60
|
-
let parsed: { agentId?: string; channel?: string; accountId?: string } = {};
|
|
60
|
+
let parsed: { agentId?: string; channel?: string; accountId?: string; instanceId?: string } = {};
|
|
61
61
|
try {
|
|
62
62
|
parsed = JSON.parse(msg.payload);
|
|
63
63
|
} catch {
|
|
@@ -65,7 +65,7 @@ export function handleP2PInbound(msg: P2PMessage, deps?: InboundHandlerDeps): vo
|
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
const { agentId, channel, accountId } = parsed;
|
|
68
|
+
const { agentId, channel, accountId, instanceId } = parsed;
|
|
69
69
|
|
|
70
70
|
if (agentId && channel && accountId) {
|
|
71
71
|
const sessionKey = buildAgentSessionKey({ agentId, channel, accountId });
|
|
@@ -74,6 +74,7 @@ export function handleP2PInbound(msg: P2PMessage, deps?: InboundHandlerDeps): vo
|
|
|
74
74
|
channel,
|
|
75
75
|
accountId,
|
|
76
76
|
sessionKey,
|
|
77
|
+
instanceId,
|
|
77
78
|
});
|
|
78
79
|
}
|
|
79
80
|
} else if (msg.type === "direct") {
|
package/src/mesh.ts
CHANGED
|
@@ -94,6 +94,8 @@ export function createMeshNetwork(options: {
|
|
|
94
94
|
const seenMessages = new Set<string>();
|
|
95
95
|
const messageHandlers = new Set<(msg: P2PMessage) => void>();
|
|
96
96
|
const topicHandlers = new Map<string, Set<(msg: string) => void>>();
|
|
97
|
+
const peerConnectHandlers = new Set<(peerId: string) => void>();
|
|
98
|
+
const peerDisconnectHandlers = new Set<(peerId: string) => void>();
|
|
97
99
|
|
|
98
100
|
function getDHTService(): ReturnType<typeof kadDHT> extends (components: infer C) => infer R ? R : never | undefined {
|
|
99
101
|
return (state.node as any)?.services?.dht;
|
|
@@ -258,11 +260,17 @@ export function createMeshNetwork(options: {
|
|
|
258
260
|
state.node.addEventListener("peer:connect", (evt) => {
|
|
259
261
|
const peerIdStr = evt.detail.toString();
|
|
260
262
|
logger?.info?.(`[libp2p-mesh] Peer connected: ${peerIdStr}`);
|
|
263
|
+
for (const handler of peerConnectHandlers) {
|
|
264
|
+
try { handler(peerIdStr); } catch {}
|
|
265
|
+
}
|
|
261
266
|
});
|
|
262
267
|
|
|
263
268
|
state.node.addEventListener("peer:disconnect", (evt) => {
|
|
264
269
|
const peerIdStr = evt.detail.toString();
|
|
265
270
|
logger?.info?.(`[libp2p-mesh] Peer disconnected: ${peerIdStr}`);
|
|
271
|
+
for (const handler of peerDisconnectHandlers) {
|
|
272
|
+
try { handler(peerIdStr); } catch {}
|
|
273
|
+
}
|
|
266
274
|
});
|
|
267
275
|
|
|
268
276
|
await state.node.handle(
|
|
@@ -667,6 +675,14 @@ export function createMeshNetwork(options: {
|
|
|
667
675
|
stop,
|
|
668
676
|
sendToPeer,
|
|
669
677
|
onMessage,
|
|
678
|
+
onPeerConnect(handler: (peerId: string) => void): () => void {
|
|
679
|
+
peerConnectHandlers.add(handler);
|
|
680
|
+
return () => { peerConnectHandlers.delete(handler); };
|
|
681
|
+
},
|
|
682
|
+
onPeerDisconnect(handler: (peerId: string) => void): () => void {
|
|
683
|
+
peerDisconnectHandlers.add(handler);
|
|
684
|
+
return () => { peerDisconnectHandlers.delete(handler); };
|
|
685
|
+
},
|
|
670
686
|
publishToTopic,
|
|
671
687
|
subscribeToTopic,
|
|
672
688
|
getLocalPeerId,
|
package/src/peer-identity.ts
CHANGED
|
@@ -3,12 +3,15 @@ export interface PeerIdentity {
|
|
|
3
3
|
channel: string;
|
|
4
4
|
accountId: string;
|
|
5
5
|
sessionKey: string;
|
|
6
|
+
/** Human-readable Instance ID (e.g. "alice-mac@AQIDBAU.7a3f9e2b") */
|
|
7
|
+
instanceId?: string;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
10
|
export interface PeerIdentityMap {
|
|
9
11
|
register(peerId: string, identity: PeerIdentity): void;
|
|
10
12
|
resolve(peerId: string): PeerIdentity | undefined;
|
|
11
13
|
resolveSessionKey(peerId: string): string | undefined;
|
|
14
|
+
resolveByInstanceId(instanceId: string): { peerId: string; identity: PeerIdentity } | undefined;
|
|
12
15
|
unregister(peerId: string): void;
|
|
13
16
|
setLocalIdentity(peerId: string, identity: PeerIdentity): void;
|
|
14
17
|
getLocalIdentity(): PeerIdentity | undefined;
|
|
@@ -18,12 +21,16 @@ export interface PeerIdentityMap {
|
|
|
18
21
|
|
|
19
22
|
export function createPeerIdentityMap(): PeerIdentityMap {
|
|
20
23
|
const peers = new Map<string, PeerIdentity>();
|
|
24
|
+
const byInstanceId = new Map<string, { peerId: string; identity: PeerIdentity }>();
|
|
21
25
|
let localPeerId: string | undefined;
|
|
22
26
|
let localIdentity: PeerIdentity | undefined;
|
|
23
27
|
|
|
24
28
|
return {
|
|
25
29
|
register(peerId: string, identity: PeerIdentity): void {
|
|
26
30
|
peers.set(peerId, identity);
|
|
31
|
+
if (identity.instanceId) {
|
|
32
|
+
byInstanceId.set(identity.instanceId, { peerId, identity });
|
|
33
|
+
}
|
|
27
34
|
},
|
|
28
35
|
|
|
29
36
|
resolve(peerId: string): PeerIdentity | undefined {
|
|
@@ -34,7 +41,15 @@ export function createPeerIdentityMap(): PeerIdentityMap {
|
|
|
34
41
|
return peers.get(peerId)?.sessionKey;
|
|
35
42
|
},
|
|
36
43
|
|
|
44
|
+
resolveByInstanceId(instanceId: string): { peerId: string; identity: PeerIdentity } | undefined {
|
|
45
|
+
return byInstanceId.get(instanceId);
|
|
46
|
+
},
|
|
47
|
+
|
|
37
48
|
unregister(peerId: string): void {
|
|
49
|
+
const identity = peers.get(peerId);
|
|
50
|
+
if (identity?.instanceId) {
|
|
51
|
+
byInstanceId.delete(identity.instanceId);
|
|
52
|
+
}
|
|
38
53
|
peers.delete(peerId);
|
|
39
54
|
},
|
|
40
55
|
|
|
@@ -42,6 +57,9 @@ export function createPeerIdentityMap(): PeerIdentityMap {
|
|
|
42
57
|
localPeerId = peerId;
|
|
43
58
|
localIdentity = identity;
|
|
44
59
|
peers.set(peerId, identity);
|
|
60
|
+
if (identity.instanceId) {
|
|
61
|
+
byInstanceId.set(identity.instanceId, { peerId, identity });
|
|
62
|
+
}
|
|
45
63
|
},
|
|
46
64
|
|
|
47
65
|
getLocalIdentity(): PeerIdentity | undefined {
|
package/src/plugin.ts
CHANGED
|
@@ -33,12 +33,16 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
|
33
33
|
start: async () => {
|
|
34
34
|
await mesh.start();
|
|
35
35
|
|
|
36
|
-
//
|
|
36
|
+
// Gather local relay identity info
|
|
37
37
|
const config = api.pluginConfig as MeshConfig | undefined;
|
|
38
38
|
const relayChannel = config?.relayChannel;
|
|
39
39
|
const relayAccountId = config?.relayAccountId;
|
|
40
|
+
const instanceIdentity = mesh.getInstanceIdentity();
|
|
41
|
+
const localInstanceId = instanceIdentity?.id;
|
|
42
|
+
const localPeerId = mesh.getLocalPeerId();
|
|
43
|
+
|
|
44
|
+
// Register local identity so remote peers can route messages back to us
|
|
40
45
|
if (relayChannel && relayAccountId) {
|
|
41
|
-
const localPeerId = mesh.getLocalPeerId();
|
|
42
46
|
const { buildAgentSessionKey } = await import("openclaw/plugin-sdk/core");
|
|
43
47
|
const sessionKey = buildAgentSessionKey({
|
|
44
48
|
agentId: api.name,
|
|
@@ -50,9 +54,10 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
|
50
54
|
channel: relayChannel,
|
|
51
55
|
accountId: relayAccountId,
|
|
52
56
|
sessionKey,
|
|
57
|
+
instanceId: localInstanceId,
|
|
53
58
|
});
|
|
54
59
|
api.logger.info?.(
|
|
55
|
-
`[libp2p-mesh] Local identity registered: agent=${api.name}, channel=${relayChannel}, account=${relayAccountId}`,
|
|
60
|
+
`[libp2p-mesh] Local identity registered: agent=${api.name}, channel=${relayChannel}, account=${relayAccountId}, instanceId=${localInstanceId}`,
|
|
56
61
|
);
|
|
57
62
|
|
|
58
63
|
// Announce identity to all currently-connected peers
|
|
@@ -61,15 +66,34 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
|
61
66
|
api.name,
|
|
62
67
|
relayChannel,
|
|
63
68
|
relayAccountId,
|
|
69
|
+
localInstanceId,
|
|
64
70
|
);
|
|
65
|
-
const
|
|
66
|
-
for (const peerId of connectedPeers) {
|
|
71
|
+
for (const peerId of mesh.getConnectedPeers()) {
|
|
67
72
|
try {
|
|
68
73
|
await mesh.sendToPeer(peerId, JSON.stringify(identityMsg));
|
|
69
74
|
} catch {
|
|
70
75
|
// Best-effort; peer may be stale in the connection list
|
|
71
76
|
}
|
|
72
77
|
}
|
|
78
|
+
|
|
79
|
+
// When a new peer connects, send our identity to them
|
|
80
|
+
mesh.onPeerConnect((peerId: string) => {
|
|
81
|
+
const msg = buildIdentityMessage(
|
|
82
|
+
localPeerId,
|
|
83
|
+
api.name,
|
|
84
|
+
relayChannel,
|
|
85
|
+
relayAccountId,
|
|
86
|
+
localInstanceId,
|
|
87
|
+
);
|
|
88
|
+
mesh.sendToPeer(peerId, JSON.stringify(msg)).catch(() => {
|
|
89
|
+
// Best-effort
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// When a peer disconnects, clean up the identity map
|
|
94
|
+
mesh.onPeerDisconnect((peerId: string) => {
|
|
95
|
+
peerIdentityMap.unregister(peerId);
|
|
96
|
+
});
|
|
73
97
|
}
|
|
74
98
|
|
|
75
99
|
// Wire up relay-aware message handler
|
|
@@ -78,7 +102,7 @@ export function registerLibp2pMesh(api: OpenClawPluginApi) {
|
|
|
78
102
|
});
|
|
79
103
|
|
|
80
104
|
const identity = mesh.getInstanceIdentity();
|
|
81
|
-
api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${
|
|
105
|
+
api.logger.info?.(`[libp2p-mesh] Service started. Peer ID: ${localPeerId}`);
|
|
82
106
|
if (identity) {
|
|
83
107
|
api.logger.info?.(`[libp2p-mesh] Instance Identity: ${identity.id}`);
|
|
84
108
|
}
|
package/src/types.ts
CHANGED
|
@@ -116,6 +116,8 @@ export interface MeshNetwork {
|
|
|
116
116
|
stop(): Promise<void>;
|
|
117
117
|
sendToPeer(peerId: string, message: string): Promise<void>;
|
|
118
118
|
onMessage(handler: (msg: P2PMessage) => void): () => void;
|
|
119
|
+
onPeerConnect(handler: (peerId: string) => void): () => void;
|
|
120
|
+
onPeerDisconnect(handler: (peerId: string) => void): () => void;
|
|
119
121
|
publishToTopic(topic: string, message: string): Promise<void>;
|
|
120
122
|
subscribeToTopic(topic: string, handler: (msg: string) => void): Promise<void>;
|
|
121
123
|
getLocalPeerId(): string;
|