libp2p-mesh 2026.6.2 → 2026.6.4
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 +81 -19
- package/api.ts +2 -0
- package/dist/api.d.ts +1 -1
- package/dist/index.js +31 -1
- package/dist/src/agent-tools.d.ts +6 -2
- package/dist/src/agent-tools.js +29 -0
- package/dist/src/cli.d.ts +15 -0
- package/dist/src/cli.js +196 -0
- package/dist/src/config-io.d.ts +9 -0
- package/dist/src/config-io.js +173 -0
- package/dist/src/inbound-delivery.d.ts +5 -3
- package/dist/src/inbound-delivery.js +35 -77
- package/dist/src/inbound.d.ts +1 -0
- package/dist/src/inbound.js +10 -3
- package/dist/src/instance-router.js +313 -37
- package/dist/src/plugin.js +34 -4
- package/dist/src/types.d.ts +18 -1
- package/dist/src/wizard.d.ts +22 -0
- package/dist/src/wizard.js +276 -0
- package/index.ts +31 -1
- package/openclaw.plugin.json +71 -1
- package/package.json +6 -5
- package/src/agent-tools.ts +36 -1
- package/src/cli.ts +226 -0
- package/src/config-io.ts +204 -0
- package/src/inbound-delivery.ts +47 -91
- package/src/inbound.ts +17 -5
- package/src/instance-router.ts +370 -39
- package/src/plugin.ts +40 -5
- package/src/types.ts +20 -1
- package/src/wizard.ts +332 -0
package/dist/src/types.d.ts
CHANGED
|
@@ -51,6 +51,18 @@ export interface UserMessagePayload {
|
|
|
51
51
|
replyTool: "p2p_send_instance_message";
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
+
export interface InboundTargetConfig {
|
|
55
|
+
id?: string;
|
|
56
|
+
channel: string;
|
|
57
|
+
target: string;
|
|
58
|
+
}
|
|
59
|
+
export interface DeliveryTargetResult {
|
|
60
|
+
id?: string;
|
|
61
|
+
channel: string;
|
|
62
|
+
target: string;
|
|
63
|
+
ok: boolean;
|
|
64
|
+
error?: string;
|
|
65
|
+
}
|
|
54
66
|
export interface DeliveryAckPayload {
|
|
55
67
|
ackFor: string;
|
|
56
68
|
ok: boolean;
|
|
@@ -58,6 +70,7 @@ export interface DeliveryAckPayload {
|
|
|
58
70
|
inboundTarget?: string;
|
|
59
71
|
deliveredAt: number;
|
|
60
72
|
error?: string;
|
|
73
|
+
results?: DeliveryTargetResult[];
|
|
61
74
|
}
|
|
62
75
|
export interface InstancePeerRecord {
|
|
63
76
|
instanceId: string;
|
|
@@ -120,6 +133,8 @@ export interface InstanceRouter {
|
|
|
120
133
|
toPeerId: string;
|
|
121
134
|
ackMessageId?: string;
|
|
122
135
|
inboundChannel?: string;
|
|
136
|
+
inboundTarget?: string;
|
|
137
|
+
deliveryResults?: DeliveryTargetResult[];
|
|
123
138
|
error?: string;
|
|
124
139
|
}>;
|
|
125
140
|
}
|
|
@@ -165,7 +180,8 @@ export interface MeshConfig {
|
|
|
165
180
|
/**
|
|
166
181
|
* Deprecated pre-2026.6 config keys kept so existing OpenClaw configs keep
|
|
167
182
|
* validating after upgrade. Relay selection is now configured with
|
|
168
|
-
* `relayList`; inbound display uses `inboundChannel`/`inboundTarget
|
|
183
|
+
* `relayList`; inbound display uses `inboundChannel`/`inboundTarget`, or
|
|
184
|
+
* `inboundTargets` when multi-target inbound delivery is enabled.
|
|
169
185
|
*/
|
|
170
186
|
relayChannel?: string;
|
|
171
187
|
relayAccountId?: string;
|
|
@@ -182,6 +198,7 @@ export interface MeshConfig {
|
|
|
182
198
|
announceAddrs?: string[];
|
|
183
199
|
inboundChannel?: string;
|
|
184
200
|
inboundTarget?: string;
|
|
201
|
+
inboundTargets?: InboundTargetConfig[];
|
|
185
202
|
deliveryAckTimeoutMs?: number;
|
|
186
203
|
}
|
|
187
204
|
export interface NATTraversalStatus {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface PromptChoice {
|
|
2
|
+
label: string;
|
|
3
|
+
value: string;
|
|
4
|
+
hint?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface WizardPrompter {
|
|
7
|
+
question(prompt: string, defaultValue?: string): Promise<string>;
|
|
8
|
+
confirm(prompt: string, defaultValue?: boolean): Promise<boolean>;
|
|
9
|
+
select(prompt: string, choices: PromptChoice[]): Promise<string>;
|
|
10
|
+
multiline(prompt: string, helpText?: string): Promise<string[]>;
|
|
11
|
+
displayBox(title: string, lines: string[]): void;
|
|
12
|
+
displaySuccess(message: string): void;
|
|
13
|
+
displayError(message: string): void;
|
|
14
|
+
displayWarning(message: string): void;
|
|
15
|
+
close(): void;
|
|
16
|
+
}
|
|
17
|
+
export declare function validateMultiaddr(raw: string): string | null;
|
|
18
|
+
export declare function createReadlinePrompter(): WizardPrompter;
|
|
19
|
+
export declare function runSetupWizard(prompter: WizardPrompter, currentConfig: Record<string, unknown>, availableChannels: string[]): Promise<Record<string, unknown>>;
|
|
20
|
+
export declare class WizardCancelledError extends Error {
|
|
21
|
+
constructor();
|
|
22
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import * as readline from "node:readline/promises";
|
|
2
|
+
import { MULTIADDR_PATTERN } from "./config-io.js";
|
|
3
|
+
// --- Validation ---
|
|
4
|
+
export function validateMultiaddr(raw) {
|
|
5
|
+
const trimmed = raw.trim();
|
|
6
|
+
if (!trimmed)
|
|
7
|
+
return "地址不能为空";
|
|
8
|
+
if (!MULTIADDR_PATTERN.test(trimmed)) {
|
|
9
|
+
return "多地址格式无效,必须以 /ip4/、/ip6/ 或 /dns/ 开头,如 /ip4/198.51.100.5/tcp/4001/p2p/12D3KooW...";
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
// --- Readline Prompter ---
|
|
14
|
+
export function createReadlinePrompter() {
|
|
15
|
+
const rl = readline.createInterface({
|
|
16
|
+
input: process.stdin,
|
|
17
|
+
output: process.stdout,
|
|
18
|
+
});
|
|
19
|
+
const displayWidth = 60;
|
|
20
|
+
function boxify(title, lines) {
|
|
21
|
+
const top = "┌" + "─".repeat(displayWidth - 2) + "┐";
|
|
22
|
+
const padTitle = "│ " + title.padEnd(displayWidth - 6) + " │";
|
|
23
|
+
const sep = "│" + " ".repeat(displayWidth - 2) + "│";
|
|
24
|
+
const bottom = "└" + "─".repeat(displayWidth - 2) + "┘";
|
|
25
|
+
console.log(top);
|
|
26
|
+
console.log(padTitle);
|
|
27
|
+
console.log(sep);
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
const padLine = "│ " + line.padEnd(displayWidth - 6) + " │";
|
|
30
|
+
console.log(padLine);
|
|
31
|
+
}
|
|
32
|
+
console.log(bottom);
|
|
33
|
+
}
|
|
34
|
+
function formatChoices(choices) {
|
|
35
|
+
return choices
|
|
36
|
+
.map((c, i) => {
|
|
37
|
+
const hint = c.hint ? `(${c.hint})` : "";
|
|
38
|
+
return ` ${i + 1}. ${c.label} ${hint}`.trimEnd();
|
|
39
|
+
})
|
|
40
|
+
.join("\n");
|
|
41
|
+
}
|
|
42
|
+
async function question(prompt, defaultValue) {
|
|
43
|
+
const suffix = defaultValue ? `(${defaultValue})` : "";
|
|
44
|
+
const answer = await rl.question(`${prompt}${suffix} → `);
|
|
45
|
+
return answer.trim() || defaultValue || "";
|
|
46
|
+
}
|
|
47
|
+
async function confirm(prompt, defaultValue) {
|
|
48
|
+
const def = defaultValue === undefined ? true : defaultValue;
|
|
49
|
+
const yn = def ? "Y/n" : "y/N";
|
|
50
|
+
const answer = await rl.question(`${prompt}(${yn})→ `);
|
|
51
|
+
const trimmed = answer.trim().toLowerCase();
|
|
52
|
+
if (trimmed === "y" || trimmed === "yes")
|
|
53
|
+
return true;
|
|
54
|
+
if (trimmed === "n" || trimmed === "no")
|
|
55
|
+
return false;
|
|
56
|
+
return def;
|
|
57
|
+
}
|
|
58
|
+
async function select(prompt, choices) {
|
|
59
|
+
console.log(`\n${prompt}`);
|
|
60
|
+
console.log(formatChoices(choices));
|
|
61
|
+
let answer = "";
|
|
62
|
+
while (true) {
|
|
63
|
+
const raw = await rl.question(` → `);
|
|
64
|
+
const num = parseInt(raw.trim(), 10);
|
|
65
|
+
if (num >= 1 && num <= choices.length) {
|
|
66
|
+
answer = choices[num - 1].value;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
console.log(` 请输入 1-${choices.length} 之间的数字。`);
|
|
70
|
+
}
|
|
71
|
+
return answer;
|
|
72
|
+
}
|
|
73
|
+
async function multiline(prompt, helpText) {
|
|
74
|
+
console.log(`\n${prompt}`);
|
|
75
|
+
if (helpText)
|
|
76
|
+
console.log(helpText);
|
|
77
|
+
const lines = [];
|
|
78
|
+
while (true) {
|
|
79
|
+
const line = await rl.question(" ");
|
|
80
|
+
if (!line.trim())
|
|
81
|
+
break;
|
|
82
|
+
const err = validateMultiaddr(line);
|
|
83
|
+
if (err) {
|
|
84
|
+
console.log(` ⚠ ${err}`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (lines.includes(line.trim())) {
|
|
88
|
+
console.log(" ⚠ 该地址已存在");
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
lines.push(line.trim());
|
|
92
|
+
}
|
|
93
|
+
if (lines.length > 0) {
|
|
94
|
+
console.log(` ✓ 已添加 ${lines.length} 个地址`);
|
|
95
|
+
}
|
|
96
|
+
return lines;
|
|
97
|
+
}
|
|
98
|
+
function displayBox(title, lines) {
|
|
99
|
+
boxify(title, lines);
|
|
100
|
+
}
|
|
101
|
+
function displaySuccess(message) {
|
|
102
|
+
console.log(` ✓ ${message}`);
|
|
103
|
+
}
|
|
104
|
+
function displayError(message) {
|
|
105
|
+
console.log(` ✗ ${message}`);
|
|
106
|
+
}
|
|
107
|
+
function displayWarning(message) {
|
|
108
|
+
console.log(` ⚠ ${message}`);
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
question,
|
|
112
|
+
confirm,
|
|
113
|
+
select,
|
|
114
|
+
multiline,
|
|
115
|
+
displayBox,
|
|
116
|
+
displaySuccess,
|
|
117
|
+
displayError,
|
|
118
|
+
displayWarning,
|
|
119
|
+
close: () => rl.close(),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// --- Setup Wizard Logic (pure — takes prompter, returns config object) ---
|
|
123
|
+
export async function runSetupWizard(prompter, currentConfig, availableChannels) {
|
|
124
|
+
const config = { ...currentConfig };
|
|
125
|
+
// --- Welcome ---
|
|
126
|
+
prompter.displayBox("🕸️ libp2p-mesh 配置向导", [
|
|
127
|
+
"我们将引导你完成 P2P Mesh 网络的基础配置。",
|
|
128
|
+
"任何时候按 Ctrl+C 可退出,配置不会被保存。",
|
|
129
|
+
]);
|
|
130
|
+
// =================================================================
|
|
131
|
+
// Layer 1: Core Path
|
|
132
|
+
// =================================================================
|
|
133
|
+
// Step 1: Discovery mode
|
|
134
|
+
const discovery = await prompter.select("选择节点发现方式:", [
|
|
135
|
+
{ value: "mdns", label: "mDNS — 局域网自动发现", hint: "默认,同一 WiFi 下推荐" },
|
|
136
|
+
{ value: "bootstrap", label: "Bootstrap — 手动指定已知节点地址", hint: "跨网络场景" },
|
|
137
|
+
{ value: "dht", label: "DHT — Kademlia 分布式发现", hint: "需要至少一个 bootstrap 入口" },
|
|
138
|
+
]);
|
|
139
|
+
config.discovery = discovery;
|
|
140
|
+
// Step 2: Bootstrap addresses (only if discovery=bootstrap or dht)
|
|
141
|
+
if (discovery === "bootstrap" || discovery === "dht") {
|
|
142
|
+
const addrs = await prompter.multiline("输入 Bootstrap 节点地址(每行一个,空行结束):", " 格式: /ip4/<IP>/tcp/<端口>/p2p/<PeerID>");
|
|
143
|
+
if (addrs.length > 0) {
|
|
144
|
+
config.bootstrapList = addrs;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Step 3: Inbound targets
|
|
148
|
+
if (availableChannels.length === 0) {
|
|
149
|
+
prompter.displayWarning("未检测到已安装的聊天频道插件。你可以稍后在 openclaw.json 中手动配置 inboundTargets。");
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const targets = [];
|
|
153
|
+
let addMore = true;
|
|
154
|
+
while (addMore) {
|
|
155
|
+
const channelChoices = availableChannels.map((ch) => ({
|
|
156
|
+
value: ch,
|
|
157
|
+
label: ch,
|
|
158
|
+
}));
|
|
159
|
+
const channel = await prompter.select("选择接收 P2P 消息的聊天频道:", channelChoices);
|
|
160
|
+
const target = await prompter.question(`输入 ${channel} 的接收目标(如 user:ou_xxx 或 chat:oc_xxx):`);
|
|
161
|
+
if (target) {
|
|
162
|
+
targets.push({ channel, target });
|
|
163
|
+
}
|
|
164
|
+
addMore = await prompter.confirm("是否添加更多接收目标?", false);
|
|
165
|
+
}
|
|
166
|
+
if (targets.length > 0) {
|
|
167
|
+
if (targets.length === 1 && !currentConfig.inboundChannel && !currentConfig.inboundTarget) {
|
|
168
|
+
// Single target: also set legacy inboundChannel/inboundTarget for backwards compat
|
|
169
|
+
config.inboundChannel = targets[0].channel;
|
|
170
|
+
config.inboundTarget = targets[0].target;
|
|
171
|
+
}
|
|
172
|
+
config.inboundTargets = targets;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Step 4: Preview core config and confirm
|
|
176
|
+
const corePreview = formatConfigPreview(config);
|
|
177
|
+
prompter.displayBox("即将写入以下配置:", corePreview);
|
|
178
|
+
const coreConfirmed = await prompter.confirm("确认写入?", true);
|
|
179
|
+
if (!coreConfirmed) {
|
|
180
|
+
prompter.displayWarning("已取消,配置未保存。");
|
|
181
|
+
throw new WizardCancelledError();
|
|
182
|
+
}
|
|
183
|
+
// =================================================================
|
|
184
|
+
// Layer 2: Advanced (optional)
|
|
185
|
+
// =================================================================
|
|
186
|
+
const wantsAdvanced = await prompter.confirm("需要在不同网络之间使用吗(跨 WiFi / 跨城市)?", false);
|
|
187
|
+
if (wantsAdvanced) {
|
|
188
|
+
// Fixed port
|
|
189
|
+
const wantFixedPort = await prompter.confirm("是否使用固定监听端口?(推荐跨网络场景)", false);
|
|
190
|
+
if (wantFixedPort) {
|
|
191
|
+
const port = await prompter.question("端口号", "4001");
|
|
192
|
+
const portNum = parseInt(port, 10);
|
|
193
|
+
if (!isNaN(portNum) && portNum > 0 && portNum < 65536) {
|
|
194
|
+
config.listenAddrs = [`/ip4/0.0.0.0/tcp/${portNum}`];
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
prompter.displayWarning("端口号无效,使用默认动态端口。");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// NAT traversal
|
|
201
|
+
const wantNAT = await prompter.confirm("是否启用 NAT 穿透?(默认开启,推荐保留)", true);
|
|
202
|
+
config.enableNATTraversal = wantNAT;
|
|
203
|
+
// Circuit Relay
|
|
204
|
+
const wantRelay = await prompter.confirm("需要配置 Circuit Relay 中继节点吗?", false);
|
|
205
|
+
if (wantRelay) {
|
|
206
|
+
const relays = await prompter.multiline("输入 Relay 节点地址(每行一个,空行结束):", " 格式: /ip4/<IP>/tcp/<端口>/p2p/<PeerID>");
|
|
207
|
+
if (relays.length > 0) {
|
|
208
|
+
config.relayList = relays;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Custom instance name
|
|
212
|
+
const wantName = await prompter.confirm("为此节点设置一个自定义名称吗?(用于 P2P 网络中的身份显示)", false);
|
|
213
|
+
if (wantName) {
|
|
214
|
+
const name = await prompter.question("节点名称");
|
|
215
|
+
if (name) {
|
|
216
|
+
config.instanceName = name;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Final preview and confirm (only if advanced config changed)
|
|
220
|
+
if (wantFixedPort || wantRelay || wantName || !config.enableNATTraversal) {
|
|
221
|
+
const finalPreview = formatConfigPreview(config);
|
|
222
|
+
prompter.displayBox("高级配置已追加,最终预览:", finalPreview);
|
|
223
|
+
const finalConfirmed = await prompter.confirm("确认写入?", true);
|
|
224
|
+
if (!finalConfirmed) {
|
|
225
|
+
prompter.displayWarning("已取消高级配置,核心配置已保存。");
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
prompter.displaySuccess("配置完成。运行 openclaw gateway restart 使配置生效。");
|
|
230
|
+
return config;
|
|
231
|
+
}
|
|
232
|
+
export class WizardCancelledError extends Error {
|
|
233
|
+
constructor() {
|
|
234
|
+
super("Wizard cancelled by user");
|
|
235
|
+
this.name = "WizardCancelledError";
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// --- Helpers ---
|
|
239
|
+
function formatConfigPreview(config) {
|
|
240
|
+
const lines = [];
|
|
241
|
+
if (config.discovery) {
|
|
242
|
+
lines.push(`discovery: ${config.discovery}`);
|
|
243
|
+
}
|
|
244
|
+
if (Array.isArray(config.bootstrapList) && config.bootstrapList.length > 0) {
|
|
245
|
+
lines.push(`bootstrapList: ${config.bootstrapList.length} 个节点`);
|
|
246
|
+
for (const addr of config.bootstrapList) {
|
|
247
|
+
lines.push(` ${addr}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (Array.isArray(config.inboundTargets) && config.inboundTargets.length > 0) {
|
|
251
|
+
lines.push("inboundTargets:");
|
|
252
|
+
for (const t of config.inboundTargets) {
|
|
253
|
+
lines.push(` - ${t.channel} / ${t.target}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else if (config.inboundChannel && config.inboundTarget) {
|
|
257
|
+
lines.push(`inboundChannel: ${config.inboundChannel}`);
|
|
258
|
+
lines.push(`inboundTarget: ${config.inboundTarget}`);
|
|
259
|
+
}
|
|
260
|
+
if (Array.isArray(config.listenAddrs) && config.listenAddrs.length > 0) {
|
|
261
|
+
lines.push(`listenAddrs: ${config.listenAddrs.join(", ")}`);
|
|
262
|
+
}
|
|
263
|
+
if (config.enableNATTraversal !== undefined) {
|
|
264
|
+
lines.push(`NAT 穿透: ${config.enableNATTraversal ? "开启" : "关闭"}`);
|
|
265
|
+
}
|
|
266
|
+
if (Array.isArray(config.relayList) && config.relayList.length > 0) {
|
|
267
|
+
lines.push(`relayList: ${config.relayList.length} 个节点`);
|
|
268
|
+
}
|
|
269
|
+
if (config.instanceName) {
|
|
270
|
+
lines.push(`instanceName: ${config.instanceName}`);
|
|
271
|
+
}
|
|
272
|
+
if (lines.length === 0) {
|
|
273
|
+
lines.push("(无配置更改)");
|
|
274
|
+
}
|
|
275
|
+
return lines;
|
|
276
|
+
}
|
package/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { definePluginEntry } from "openclaw/plugin-sdk/core";
|
|
2
2
|
import type { OpenClawPluginConfigSchema } from "openclaw/plugin-sdk/core";
|
|
3
3
|
import { registerLibp2pMesh } from "./src/plugin.js";
|
|
4
|
+
import { registerLibp2pMeshCli } from "./src/cli.js";
|
|
4
5
|
|
|
5
6
|
function createLibp2pMeshConfigSchema(): OpenClawPluginConfigSchema {
|
|
6
7
|
return {
|
|
@@ -129,6 +130,29 @@ function createLibp2pMeshConfigSchema(): OpenClawPluginConfigSchema {
|
|
|
129
130
|
type: "string",
|
|
130
131
|
description: "OpenClaw channel target for inbound P2P user messages, for example user:ou_xxx or chat:oc_xxx.",
|
|
131
132
|
},
|
|
133
|
+
inboundTargets: {
|
|
134
|
+
type: "array",
|
|
135
|
+
description: "OpenClaw channel targets used to display inbound P2P user messages. When present, this overrides inboundChannel/inboundTarget. An empty array disables inbound delivery.",
|
|
136
|
+
items: {
|
|
137
|
+
type: "object",
|
|
138
|
+
additionalProperties: false,
|
|
139
|
+
properties: {
|
|
140
|
+
id: {
|
|
141
|
+
type: "string",
|
|
142
|
+
description: "Optional local display name used in logs and delivery ACK output.",
|
|
143
|
+
},
|
|
144
|
+
channel: {
|
|
145
|
+
type: "string",
|
|
146
|
+
description: "OpenClaw channel used to display this inbound P2P user message, for example \"feishu\".",
|
|
147
|
+
},
|
|
148
|
+
target: {
|
|
149
|
+
type: "string",
|
|
150
|
+
description: "OpenClaw channel target for this inbound P2P user message, for example user:ou_xxx or chat:oc_xxx.",
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
required: ["channel", "target"],
|
|
154
|
+
},
|
|
155
|
+
},
|
|
132
156
|
deliveryAckTimeoutMs: {
|
|
133
157
|
type: "number",
|
|
134
158
|
default: 15000,
|
|
@@ -144,5 +168,11 @@ export default definePluginEntry({
|
|
|
144
168
|
name: "libp2p Mesh Network",
|
|
145
169
|
description: "P2P network for cross-instance agent communication via libp2p.",
|
|
146
170
|
configSchema: createLibp2pMeshConfigSchema(),
|
|
147
|
-
register:
|
|
171
|
+
register: (api) => {
|
|
172
|
+
registerLibp2pMesh(api);
|
|
173
|
+
// 5. Register CLI commands (setup wizard + config management)
|
|
174
|
+
api.registerCli(registerLibp2pMeshCli, {
|
|
175
|
+
commands: ["libp2p-mesh"],
|
|
176
|
+
});
|
|
177
|
+
},
|
|
148
178
|
});
|
package/openclaw.plugin.json
CHANGED
|
@@ -11,7 +11,54 @@
|
|
|
11
11
|
"schema": {
|
|
12
12
|
"type": "object",
|
|
13
13
|
"additionalProperties": false,
|
|
14
|
-
"properties": {
|
|
14
|
+
"properties": {
|
|
15
|
+
"listenAddrs": {
|
|
16
|
+
"type": "array",
|
|
17
|
+
"items": { "type": "string" },
|
|
18
|
+
"default": ["/ip4/0.0.0.0/tcp/0"]
|
|
19
|
+
},
|
|
20
|
+
"enableWebSocket": {
|
|
21
|
+
"type": "boolean",
|
|
22
|
+
"default": false,
|
|
23
|
+
"description": "Enable WebSocket transport"
|
|
24
|
+
},
|
|
25
|
+
"discovery": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"enum": ["mdns", "bootstrap", "dht"],
|
|
28
|
+
"default": "mdns"
|
|
29
|
+
},
|
|
30
|
+
"bootstrapList": {
|
|
31
|
+
"type": "array",
|
|
32
|
+
"items": { "type": "string" }
|
|
33
|
+
},
|
|
34
|
+
"meshTopic": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"default": "openclaw-mesh"
|
|
37
|
+
},
|
|
38
|
+
"inboundTargets": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"description": "OpenClaw channel targets used to display inbound P2P user messages. When present, this overrides inboundChannel/inboundTarget. An empty array disables inbound delivery.",
|
|
41
|
+
"items": {
|
|
42
|
+
"type": "object",
|
|
43
|
+
"additionalProperties": false,
|
|
44
|
+
"properties": {
|
|
45
|
+
"id": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"description": "Optional local display name used in logs and delivery ACK output."
|
|
48
|
+
},
|
|
49
|
+
"channel": {
|
|
50
|
+
"type": "string",
|
|
51
|
+
"description": "OpenClaw channel used to display this inbound P2P user message, for example \"feishu\"."
|
|
52
|
+
},
|
|
53
|
+
"target": {
|
|
54
|
+
"type": "string",
|
|
55
|
+
"description": "OpenClaw channel target for this inbound P2P user message, for example user:ou_xxx or chat:oc_xxx."
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"required": ["channel", "target"]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
15
62
|
}
|
|
16
63
|
}
|
|
17
64
|
},
|
|
@@ -130,6 +177,29 @@
|
|
|
130
177
|
"type": "string",
|
|
131
178
|
"description": "OpenClaw channel target for inbound P2P user messages, for example user:ou_xxx or chat:oc_xxx."
|
|
132
179
|
},
|
|
180
|
+
"inboundTargets": {
|
|
181
|
+
"type": "array",
|
|
182
|
+
"description": "OpenClaw channel targets used to display inbound P2P user messages. When present, this overrides inboundChannel/inboundTarget. An empty array disables inbound delivery.",
|
|
183
|
+
"items": {
|
|
184
|
+
"type": "object",
|
|
185
|
+
"additionalProperties": false,
|
|
186
|
+
"properties": {
|
|
187
|
+
"id": {
|
|
188
|
+
"type": "string",
|
|
189
|
+
"description": "Optional local display name used in logs and delivery ACK output."
|
|
190
|
+
},
|
|
191
|
+
"channel": {
|
|
192
|
+
"type": "string",
|
|
193
|
+
"description": "OpenClaw channel used to display this inbound P2P user message, for example \"feishu\"."
|
|
194
|
+
},
|
|
195
|
+
"target": {
|
|
196
|
+
"type": "string",
|
|
197
|
+
"description": "OpenClaw channel target for this inbound P2P user message, for example user:ou_xxx or chat:oc_xxx."
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
"required": ["channel", "target"]
|
|
201
|
+
}
|
|
202
|
+
},
|
|
133
203
|
"deliveryAckTimeoutMs": {
|
|
134
204
|
"type": "number",
|
|
135
205
|
"default": 15000,
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libp2p-mesh",
|
|
3
|
-
"version": "2026.6.
|
|
3
|
+
"version": "2026.6.4",
|
|
4
4
|
"description": "OpenClaw libp2p P2P mesh network plugin for cross-instance agent communication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"scripts": {
|
|
9
9
|
"build": "tsc -p tsconfig.json",
|
|
10
|
+
"test": "node --import tsx --tSest test/*.test.ts",
|
|
10
11
|
"prepack": "npm run build"
|
|
11
12
|
},
|
|
12
13
|
"files": [
|
|
@@ -63,7 +64,7 @@
|
|
|
63
64
|
"systemImage": "network"
|
|
64
65
|
},
|
|
65
66
|
"install": {
|
|
66
|
-
"npmSpec": "
|
|
67
|
+
"npmSpec": "libp2p-mesh",
|
|
67
68
|
"defaultChoice": "npm",
|
|
68
69
|
"minHostVersion": ">=2026.3.24"
|
|
69
70
|
},
|
|
@@ -96,12 +97,12 @@
|
|
|
96
97
|
"license": "MIT",
|
|
97
98
|
"repository": {
|
|
98
99
|
"type": "git",
|
|
99
|
-
"url": "git+https://github.com/
|
|
100
|
+
"url": "git+https://github.com/XM-ls/openclaw-libp2p-mesh.git"
|
|
100
101
|
},
|
|
101
102
|
"bugs": {
|
|
102
|
-
"url": "https://github.com/
|
|
103
|
+
"url": "https://github.com/XM-ls/openclaw-libp2p-mesh/issues"
|
|
103
104
|
},
|
|
104
|
-
"homepage": "https://github.com/
|
|
105
|
+
"homepage": "https://github.com/XM-ls/openclaw-libp2p-mesh#readme",
|
|
105
106
|
"pnpm": {
|
|
106
107
|
"onlyBuiltDependencies": [
|
|
107
108
|
"@google/genai",
|
package/src/agent-tools.ts
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
|
-
import type { InstanceRouter, MeshNetwork } from "./types.js";
|
|
1
|
+
import type { DeliveryTargetResult, InstanceRouter, MeshNetwork } from "./types.js";
|
|
2
|
+
|
|
3
|
+
function targetLabel(result: DeliveryTargetResult): string {
|
|
4
|
+
const id = result.id?.trim();
|
|
5
|
+
const location = `${result.channel} / ${result.target}`;
|
|
6
|
+
return id ? `${id} (${location})` : location;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function formatDeliveryResults(
|
|
10
|
+
instanceId: string,
|
|
11
|
+
delivered: boolean,
|
|
12
|
+
results: DeliveryTargetResult[],
|
|
13
|
+
): string {
|
|
14
|
+
const header = delivered
|
|
15
|
+
? `发往 ${instanceId} 的消息投递结果:`
|
|
16
|
+
: `发往 ${instanceId} 的消息投递失败:`;
|
|
17
|
+
const lines = results.map((result) => {
|
|
18
|
+
if (result.ok) {
|
|
19
|
+
return `- ${targetLabel(result)}:已送达`;
|
|
20
|
+
}
|
|
21
|
+
return `- ${targetLabel(result)}:失败:${result.error ?? "unknown error"}`;
|
|
22
|
+
});
|
|
23
|
+
return [header, ...lines].join("\n");
|
|
24
|
+
}
|
|
2
25
|
|
|
3
26
|
export function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter) {
|
|
4
27
|
return [
|
|
@@ -323,6 +346,18 @@ export function buildP2PTools(mesh: MeshNetwork, router?: InstanceRouter) {
|
|
|
323
346
|
};
|
|
324
347
|
}
|
|
325
348
|
const result = await router.sendInstanceMessage(instanceId, message);
|
|
349
|
+
if (result.deliveryResults && result.deliveryResults.length > 0) {
|
|
350
|
+
return {
|
|
351
|
+
content: [
|
|
352
|
+
{
|
|
353
|
+
type: "text" as const,
|
|
354
|
+
text: formatDeliveryResults(instanceId, result.delivered, result.deliveryResults),
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
details: result,
|
|
358
|
+
isError: result.delivered ? undefined : true,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
326
361
|
if (!result.delivered) {
|
|
327
362
|
return {
|
|
328
363
|
content: [
|