libp2p-mesh 2026.6.4 → 2026.6.6
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/plugin.js +1 -1
- package/dist/src/wizard.d.ts +12 -3
- package/dist/src/wizard.js +438 -159
- package/package.json +1 -1
- package/src/plugin.ts +1 -1
- package/src/wizard.ts +574 -173
package/dist/src/wizard.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import * as readline from "node:readline
|
|
1
|
+
import * as readline from "node:readline";
|
|
2
2
|
import { MULTIADDR_PATTERN } from "./config-io.js";
|
|
3
|
+
// --- Constants ---
|
|
4
|
+
const SENTINEL_SKIP = "__wizard_skip__";
|
|
5
|
+
const SENTINEL_FINISH = "__wizard_finish__";
|
|
3
6
|
// --- Validation ---
|
|
4
7
|
export function validateMultiaddr(raw) {
|
|
5
8
|
const trimmed = raw.trim();
|
|
@@ -10,12 +13,163 @@ export function validateMultiaddr(raw) {
|
|
|
10
13
|
}
|
|
11
14
|
return null;
|
|
12
15
|
}
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Interactive arrow-key selector (with non-TTY fallback)
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
function isTTY() {
|
|
20
|
+
return Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Move cursor up `n` lines using ANSI escape (no-op on dumb terminals).
|
|
24
|
+
*/
|
|
25
|
+
function ansiUp(n) {
|
|
26
|
+
if (n <= 0)
|
|
27
|
+
return;
|
|
28
|
+
process.stdout.write(`\x1b[${n}A`);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Clear current line from cursor to end.
|
|
32
|
+
*/
|
|
33
|
+
function ansiClearLine() {
|
|
34
|
+
process.stdout.write("\x1b[K");
|
|
35
|
+
}
|
|
36
|
+
function renderChoices(choices, selectedIdx) {
|
|
37
|
+
for (let i = 0; i < choices.length; i++) {
|
|
38
|
+
const c = choices[i];
|
|
39
|
+
const pointer = i === selectedIdx ? "❯" : " ";
|
|
40
|
+
const hint = c.hint ? `(${c.hint})` : "";
|
|
41
|
+
const line = `${pointer} ${c.label} ${hint}`.trimEnd();
|
|
42
|
+
if (i === selectedIdx) {
|
|
43
|
+
process.stdout.write(`\x1b[7m${line}\x1b[0m\n`);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
process.stdout.write(`${line}\n`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function reRenderChoices(choices, selectedIdx) {
|
|
51
|
+
const n = choices.length;
|
|
52
|
+
ansiUp(n);
|
|
53
|
+
for (let i = 0; i < n; i++) {
|
|
54
|
+
ansiClearLine();
|
|
55
|
+
const c = choices[i];
|
|
56
|
+
const pointer = i === selectedIdx ? "❯" : " ";
|
|
57
|
+
const hint = c.hint ? `(${c.hint})` : "";
|
|
58
|
+
const line = `${pointer} ${c.label} ${hint}`.trimEnd();
|
|
59
|
+
if (i === selectedIdx) {
|
|
60
|
+
process.stdout.write(`\x1b[7m${line}\x1b[0m\n`);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
process.stdout.write(`${line}\n`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function clearChoices(count) {
|
|
68
|
+
ansiUp(count);
|
|
69
|
+
for (let i = 0; i < count; i++) {
|
|
70
|
+
ansiClearLine();
|
|
71
|
+
process.stdout.write("\n");
|
|
72
|
+
}
|
|
73
|
+
// Now we're back where the first choice was rendered
|
|
74
|
+
ansiUp(count);
|
|
75
|
+
}
|
|
76
|
+
function interactiveSelect(prompt, choices) {
|
|
77
|
+
let selectedIdx = 0;
|
|
78
|
+
// Print prompt + blank line
|
|
79
|
+
process.stdout.write(`\n${prompt}\n`);
|
|
80
|
+
// Initial render
|
|
81
|
+
renderChoices(choices, selectedIdx);
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
const wasRaw = process.stdin.isRaw;
|
|
84
|
+
process.stdin.setRawMode(true);
|
|
85
|
+
readline.emitKeypressEvents(process.stdin);
|
|
86
|
+
const onKeypress = (_str, key) => {
|
|
87
|
+
if (!key)
|
|
88
|
+
return;
|
|
89
|
+
if (key.name === "up" || key.name === "k") {
|
|
90
|
+
selectedIdx =
|
|
91
|
+
(selectedIdx - 1 + choices.length) % choices.length;
|
|
92
|
+
reRenderChoices(choices, selectedIdx);
|
|
93
|
+
}
|
|
94
|
+
else if (key.name === "down" || key.name === "j") {
|
|
95
|
+
selectedIdx = (selectedIdx + 1) % choices.length;
|
|
96
|
+
reRenderChoices(choices, selectedIdx);
|
|
97
|
+
}
|
|
98
|
+
else if (key.name === "return" || key.name === "space") {
|
|
99
|
+
const chosen = choices[selectedIdx];
|
|
100
|
+
cleanup();
|
|
101
|
+
clearChoices(choices.length);
|
|
102
|
+
process.stdout.write(` → ${chosen.label}\n`);
|
|
103
|
+
resolve(chosen.value);
|
|
104
|
+
}
|
|
105
|
+
else if (key.ctrl && key.name === "c") {
|
|
106
|
+
cleanup();
|
|
107
|
+
process.stdout.write("\n");
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const cleanup = () => {
|
|
112
|
+
process.stdin.setRawMode(wasRaw ?? false);
|
|
113
|
+
process.stdin.removeListener("keypress", onKeypress);
|
|
114
|
+
};
|
|
115
|
+
process.stdin.on("keypress", onKeypress);
|
|
18
116
|
});
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Plain numbered-list fallback when stdin is not a TTY (e.g. piped input).
|
|
120
|
+
*/
|
|
121
|
+
function fallbackNumberedSelect(prompt, choices) {
|
|
122
|
+
return new Promise((resolve) => {
|
|
123
|
+
const rl = readline.createInterface({
|
|
124
|
+
input: process.stdin,
|
|
125
|
+
output: process.stdout,
|
|
126
|
+
});
|
|
127
|
+
console.log(`\n${prompt}`);
|
|
128
|
+
const formatted = choices
|
|
129
|
+
.map((c, i) => {
|
|
130
|
+
const hint = c.hint ? `(${c.hint})` : "";
|
|
131
|
+
return ` ${i + 1}. ${c.label} ${hint}`.trimEnd();
|
|
132
|
+
})
|
|
133
|
+
.join("\n");
|
|
134
|
+
console.log(formatted);
|
|
135
|
+
const ask = () => {
|
|
136
|
+
rl.question(` → `, (answer) => {
|
|
137
|
+
const num = parseInt(answer.trim(), 10);
|
|
138
|
+
if (num >= 1 && num <= choices.length) {
|
|
139
|
+
rl.close();
|
|
140
|
+
resolve(choices[num - 1].value);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
console.log(` 请输入 1-${choices.length} 之间的数字。`);
|
|
144
|
+
ask();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
};
|
|
148
|
+
ask();
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// Readline Prompter factory
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
export function createReadlinePrompter() {
|
|
155
|
+
let rl = null;
|
|
156
|
+
/** Lazy-create a readline interface (used by question / confirm). */
|
|
157
|
+
function ensureRL() {
|
|
158
|
+
if (!rl) {
|
|
159
|
+
rl = readline.createInterface({
|
|
160
|
+
input: process.stdin,
|
|
161
|
+
output: process.stdout,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return rl;
|
|
165
|
+
}
|
|
166
|
+
/** Dispose the active readline interface so it doesn't conflict with raw-mode selection. */
|
|
167
|
+
function disposeRL() {
|
|
168
|
+
if (rl) {
|
|
169
|
+
rl.close();
|
|
170
|
+
rl = null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
19
173
|
const displayWidth = 60;
|
|
20
174
|
function boxify(title, lines) {
|
|
21
175
|
const top = "┌" + "─".repeat(displayWidth - 2) + "┐";
|
|
@@ -31,69 +185,113 @@ export function createReadlinePrompter() {
|
|
|
31
185
|
}
|
|
32
186
|
console.log(bottom);
|
|
33
187
|
}
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
188
|
+
async function question(prompt, defaultValue, options) {
|
|
189
|
+
const hints = [];
|
|
190
|
+
if (options?.allowSkip)
|
|
191
|
+
hints.push("回车跳过,! 完成配置");
|
|
192
|
+
if (defaultValue)
|
|
193
|
+
hints.push(`${defaultValue}`);
|
|
194
|
+
const suffix = hints.length > 0 ? `(${hints.join(",")})` : "";
|
|
195
|
+
const rli = ensureRL();
|
|
196
|
+
return new Promise((resolve) => {
|
|
197
|
+
rli.question(`${prompt}${suffix} → `, (answer) => {
|
|
198
|
+
const trimmed = answer.trim();
|
|
199
|
+
if (options?.allowSkip && trimmed === "!") {
|
|
200
|
+
resolve(SENTINEL_FINISH);
|
|
201
|
+
}
|
|
202
|
+
else if (!trimmed && options?.allowSkip) {
|
|
203
|
+
resolve(SENTINEL_SKIP);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
resolve(trimmed || defaultValue || "");
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
});
|
|
46
210
|
}
|
|
47
|
-
async function confirm(prompt, defaultValue) {
|
|
211
|
+
async function confirm(prompt, defaultValue, options) {
|
|
48
212
|
const def = defaultValue === undefined ? true : defaultValue;
|
|
49
213
|
const yn = def ? "Y/n" : "y/N";
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
214
|
+
const extra = options?.allowSkip ? ",F=完成,S=跳过" : "";
|
|
215
|
+
const rli = ensureRL();
|
|
216
|
+
return new Promise((resolve) => {
|
|
217
|
+
rli.question(`${prompt}(${yn}${extra})→ `, (answer) => {
|
|
218
|
+
const trimmed = answer.trim().toLowerCase();
|
|
219
|
+
if (options?.allowSkip && (trimmed === "f" || trimmed === "finish")) {
|
|
220
|
+
resolve(SENTINEL_FINISH);
|
|
221
|
+
}
|
|
222
|
+
else if (options?.allowSkip && (trimmed === "s" || trimmed === "skip")) {
|
|
223
|
+
resolve(null);
|
|
224
|
+
}
|
|
225
|
+
else if (trimmed === "y" || trimmed === "yes") {
|
|
226
|
+
resolve(true);
|
|
227
|
+
}
|
|
228
|
+
else if (trimmed === "n" || trimmed === "no") {
|
|
229
|
+
resolve(false);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
resolve(def);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
});
|
|
57
236
|
}
|
|
58
|
-
async function select(prompt, choices) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
237
|
+
async function select(prompt, choices, options) {
|
|
238
|
+
const allChoices = options?.includeSkip
|
|
239
|
+
? [
|
|
240
|
+
...choices,
|
|
241
|
+
{
|
|
242
|
+
value: SENTINEL_FINISH,
|
|
243
|
+
label: "✓ Finish — 保存并退出",
|
|
244
|
+
hint: "",
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
value: SENTINEL_SKIP,
|
|
248
|
+
label: "⊘ Skip — 先不配置",
|
|
249
|
+
hint: "",
|
|
250
|
+
},
|
|
251
|
+
]
|
|
252
|
+
: choices;
|
|
253
|
+
// Release readline so it doesn't fight over stdin in raw mode
|
|
254
|
+
disposeRL();
|
|
255
|
+
const result = isTTY()
|
|
256
|
+
? await interactiveSelect(prompt, allChoices)
|
|
257
|
+
: await fallbackNumberedSelect(prompt, allChoices);
|
|
258
|
+
if (result === SENTINEL_FINISH)
|
|
259
|
+
throw new WizardFinishError();
|
|
260
|
+
return result;
|
|
72
261
|
}
|
|
73
262
|
async function multiline(prompt, helpText) {
|
|
74
263
|
console.log(`\n${prompt}`);
|
|
75
264
|
if (helpText)
|
|
76
265
|
console.log(helpText);
|
|
77
266
|
const lines = [];
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
267
|
+
const rli = ensureRL();
|
|
268
|
+
return new Promise((resolve) => {
|
|
269
|
+
const ask = () => {
|
|
270
|
+
rli.question(" ", (line) => {
|
|
271
|
+
if (!line.trim()) {
|
|
272
|
+
if (lines.length > 0) {
|
|
273
|
+
console.log(` ✓ 已添加 ${lines.length} 个地址`);
|
|
274
|
+
}
|
|
275
|
+
resolve(lines);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const err = validateMultiaddr(line);
|
|
279
|
+
if (err) {
|
|
280
|
+
console.log(` ⚠ ${err}`);
|
|
281
|
+
ask();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (lines.includes(line.trim())) {
|
|
285
|
+
console.log(" ⚠ 该地址已存在");
|
|
286
|
+
ask();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
lines.push(line.trim());
|
|
290
|
+
ask();
|
|
291
|
+
});
|
|
292
|
+
};
|
|
293
|
+
ask();
|
|
294
|
+
});
|
|
97
295
|
}
|
|
98
296
|
function displayBox(title, lines) {
|
|
99
297
|
boxify(title, lines);
|
|
@@ -116,118 +314,189 @@ export function createReadlinePrompter() {
|
|
|
116
314
|
displaySuccess,
|
|
117
315
|
displayError,
|
|
118
316
|
displayWarning,
|
|
119
|
-
close: () =>
|
|
317
|
+
close: () => disposeRL(),
|
|
120
318
|
};
|
|
121
319
|
}
|
|
122
|
-
//
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
// Setup Wizard Logic (pure — takes prompter, returns config object)
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
123
323
|
export async function runSetupWizard(prompter, currentConfig, availableChannels) {
|
|
124
324
|
const config = { ...currentConfig };
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
325
|
+
const isSkip = (v) => v === SENTINEL_SKIP || v === null;
|
|
326
|
+
const assertNotFinished = (v) => {
|
|
327
|
+
if (v === SENTINEL_FINISH)
|
|
328
|
+
throw new WizardFinishError();
|
|
329
|
+
};
|
|
330
|
+
try {
|
|
331
|
+
// --- Welcome ---
|
|
332
|
+
prompter.displayBox("🕸️ libp2p-mesh 配置向导", [
|
|
333
|
+
"我们将引导你完成 P2P Mesh 网络的基础配置。",
|
|
334
|
+
"任何时候按 Ctrl+C 可退出,配置不会被保存。",
|
|
335
|
+
]);
|
|
336
|
+
// =================================================================
|
|
337
|
+
// Layer 1: Core Path
|
|
338
|
+
// =================================================================
|
|
339
|
+
// Step 1: Discovery mode (select throws WizardFinishError on Finish)
|
|
340
|
+
const discovery = await prompter.select("选择节点发现方式:", [
|
|
341
|
+
{
|
|
342
|
+
value: "mdns",
|
|
343
|
+
label: "mDNS — 局域网自动发现",
|
|
344
|
+
hint: "默认,同一 WiFi 下推荐",
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
value: "bootstrap",
|
|
348
|
+
label: "Bootstrap — 手动指定已知节点地址",
|
|
349
|
+
hint: "跨网络场景",
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
value: "dht",
|
|
353
|
+
label: "DHT — Kademlia 分布式发现",
|
|
354
|
+
hint: "需要至少一个 bootstrap 入口",
|
|
355
|
+
},
|
|
356
|
+
], { includeSkip: true });
|
|
357
|
+
if (!isSkip(discovery)) {
|
|
358
|
+
config.discovery = discovery;
|
|
359
|
+
// Step 2: Bootstrap addresses (only if discovery=bootstrap or dht)
|
|
360
|
+
if (discovery === "bootstrap" || discovery === "dht") {
|
|
361
|
+
const addrs = await prompter.multiline("输入 Bootstrap 节点地址(每行一个,空行结束):", " 格式: /ip4/<IP>/tcp/<端口>/p2p/<PeerID>");
|
|
362
|
+
if (addrs.length > 0) {
|
|
363
|
+
config.bootstrapList = addrs;
|
|
364
|
+
}
|
|
163
365
|
}
|
|
164
|
-
addMore = await prompter.confirm("是否添加更多接收目标?", false);
|
|
165
366
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
367
|
+
// Step 3: Inbound targets
|
|
368
|
+
if (availableChannels.length === 0) {
|
|
369
|
+
prompter.displayWarning("未检测到已安装的聊天频道插件。你可以稍后在 openclaw.json 中手动配置 inboundTargets。");
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
const wantInbound = await prompter.confirm("是否配置 P2P 消息的接收目标?", false, { allowSkip: true });
|
|
373
|
+
assertNotFinished(wantInbound);
|
|
374
|
+
if (wantInbound === true) {
|
|
375
|
+
const targets = [];
|
|
376
|
+
let addMore = true;
|
|
377
|
+
while (addMore) {
|
|
378
|
+
const channelChoices = availableChannels.map((ch) => ({
|
|
379
|
+
value: ch,
|
|
380
|
+
label: ch,
|
|
381
|
+
}));
|
|
382
|
+
// select throws WizardFinishError on Finish
|
|
383
|
+
const channel = await prompter.select("选择接收 P2P 消息的聊天频道:", channelChoices, { includeSkip: true });
|
|
384
|
+
if (isSkip(channel))
|
|
385
|
+
break;
|
|
386
|
+
const target = await prompter.question(`输入 ${channel} 的接收目标(如 user:ou_xxx 或 chat:oc_xxx):`, undefined, { allowSkip: true });
|
|
387
|
+
assertNotFinished(target);
|
|
388
|
+
if (isSkip(target))
|
|
389
|
+
break;
|
|
390
|
+
if (target) {
|
|
391
|
+
targets.push({ channel, target });
|
|
392
|
+
}
|
|
393
|
+
const more = await prompter.confirm("是否添加更多接收目标?", false);
|
|
394
|
+
assertNotFinished(more);
|
|
395
|
+
addMore = more ?? false;
|
|
396
|
+
}
|
|
397
|
+
if (targets.length > 0) {
|
|
398
|
+
if (targets.length === 1 &&
|
|
399
|
+
!currentConfig.inboundChannel &&
|
|
400
|
+
!currentConfig.inboundTarget) {
|
|
401
|
+
// Single target: also set legacy inboundChannel/inboundTarget for backwards compat
|
|
402
|
+
config.inboundChannel = targets[0].channel;
|
|
403
|
+
config.inboundTarget = targets[0].target;
|
|
404
|
+
}
|
|
405
|
+
config.inboundTargets = targets;
|
|
406
|
+
}
|
|
171
407
|
}
|
|
172
|
-
|
|
408
|
+
// wantInbound === null (skip) or false (no) → skip inbound config
|
|
173
409
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
410
|
+
// Step 4: Preview core config and confirm
|
|
411
|
+
const corePreview = formatConfigPreview(config);
|
|
412
|
+
prompter.displayBox("即将写入以下配置:", corePreview);
|
|
413
|
+
const coreConfirmed = await prompter.confirm("确认写入?", true);
|
|
414
|
+
if (!coreConfirmed) {
|
|
415
|
+
prompter.displayWarning("已取消,配置未保存。");
|
|
416
|
+
throw new WizardCancelledError();
|
|
417
|
+
}
|
|
418
|
+
// =================================================================
|
|
419
|
+
// Layer 2: Advanced (optional)
|
|
420
|
+
// =================================================================
|
|
421
|
+
const wantsAdvanced = await prompter.confirm("需要在不同网络之间使用吗(跨 WiFi / 跨城市)?", false, { allowSkip: true });
|
|
422
|
+
assertNotFinished(wantsAdvanced);
|
|
423
|
+
if (wantsAdvanced === true) {
|
|
424
|
+
// Fixed port
|
|
425
|
+
const wantFixedPort = await prompter.confirm("是否使用固定监听端口?(推荐跨网络场景)", false, { allowSkip: true });
|
|
426
|
+
assertNotFinished(wantFixedPort);
|
|
427
|
+
if (wantFixedPort === true) {
|
|
428
|
+
const port = await prompter.question("端口号", "4001", {
|
|
429
|
+
allowSkip: true,
|
|
430
|
+
});
|
|
431
|
+
assertNotFinished(port);
|
|
432
|
+
if (!isSkip(port)) {
|
|
433
|
+
const portNum = parseInt(port, 10);
|
|
434
|
+
if (!isNaN(portNum) && portNum > 0 && portNum < 65536) {
|
|
435
|
+
config.listenAddrs = [`/ip4/0.0.0.0/tcp/${portNum}`];
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
prompter.displayWarning("端口号无效,使用默认动态端口。");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
195
441
|
}
|
|
196
|
-
|
|
197
|
-
|
|
442
|
+
// NAT traversal
|
|
443
|
+
const wantNAT = await prompter.confirm("是否启用 NAT 穿透?(默认开启,推荐保留)", true, { allowSkip: true });
|
|
444
|
+
assertNotFinished(wantNAT);
|
|
445
|
+
if (wantNAT !== null) {
|
|
446
|
+
config.enableNATTraversal = wantNAT;
|
|
198
447
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (relays.length > 0) {
|
|
208
|
-
config.relayList = relays;
|
|
448
|
+
// Circuit Relay
|
|
449
|
+
const wantRelay = await prompter.confirm("需要配置 Circuit Relay 中继节点吗?", false, { allowSkip: true });
|
|
450
|
+
assertNotFinished(wantRelay);
|
|
451
|
+
if (wantRelay === true) {
|
|
452
|
+
const relays = await prompter.multiline("输入 Relay 节点地址(每行一个,空行结束):", " 格式: /ip4/<IP>/tcp/<端口>/p2p/<PeerID>");
|
|
453
|
+
if (relays.length > 0) {
|
|
454
|
+
config.relayList = relays;
|
|
455
|
+
}
|
|
209
456
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
457
|
+
// Custom instance name
|
|
458
|
+
const wantName = await prompter.confirm("为此节点设置一个自定义名称吗?(用于 P2P 网络中的身份显示)", false, { allowSkip: true });
|
|
459
|
+
assertNotFinished(wantName);
|
|
460
|
+
if (wantName === true) {
|
|
461
|
+
const name = await prompter.question("节点名称", undefined, {
|
|
462
|
+
allowSkip: true,
|
|
463
|
+
});
|
|
464
|
+
assertNotFinished(name);
|
|
465
|
+
if (!isSkip(name) && name) {
|
|
466
|
+
config.instanceName = name;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// Final preview and confirm (only if advanced config changed)
|
|
470
|
+
if (wantFixedPort === true ||
|
|
471
|
+
wantRelay === true ||
|
|
472
|
+
wantName === true ||
|
|
473
|
+
(wantNAT !== null && !config.enableNATTraversal)) {
|
|
474
|
+
const finalPreview = formatConfigPreview(config);
|
|
475
|
+
prompter.displayBox("高级配置已追加,最终预览:", finalPreview);
|
|
476
|
+
const finalConfirmed = await prompter.confirm("确认写入?", true);
|
|
477
|
+
if (!finalConfirmed) {
|
|
478
|
+
prompter.displayWarning("已取消高级配置,核心配置已保存。");
|
|
479
|
+
}
|
|
217
480
|
}
|
|
218
481
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
482
|
+
prompter.displaySuccess("配置完成。运行 openclaw gateway restart 使配置生效。");
|
|
483
|
+
return config;
|
|
484
|
+
}
|
|
485
|
+
catch (err) {
|
|
486
|
+
if (err instanceof WizardFinishError) {
|
|
487
|
+
// User chose Finish — show accumulated config and save
|
|
488
|
+
const finishPreview = formatConfigPreview(config);
|
|
489
|
+
prompter.displayBox("配置完成,即将写入:", finishPreview);
|
|
490
|
+
const confirmed = await prompter.confirm("确认写入?", true);
|
|
491
|
+
if (!confirmed) {
|
|
492
|
+
prompter.displayWarning("已取消,配置未保存。");
|
|
493
|
+
throw new WizardCancelledError();
|
|
226
494
|
}
|
|
495
|
+
prompter.displaySuccess("配置完成。运行 openclaw gateway restart 使配置生效。");
|
|
496
|
+
return config;
|
|
227
497
|
}
|
|
498
|
+
throw err;
|
|
228
499
|
}
|
|
229
|
-
prompter.displaySuccess("配置完成。运行 openclaw gateway restart 使配置生效。");
|
|
230
|
-
return config;
|
|
231
500
|
}
|
|
232
501
|
export class WizardCancelledError extends Error {
|
|
233
502
|
constructor() {
|
|
@@ -235,19 +504,27 @@ export class WizardCancelledError extends Error {
|
|
|
235
504
|
this.name = "WizardCancelledError";
|
|
236
505
|
}
|
|
237
506
|
}
|
|
507
|
+
export class WizardFinishError extends Error {
|
|
508
|
+
constructor() {
|
|
509
|
+
super("Wizard finished early by user");
|
|
510
|
+
this.name = "WizardFinishError";
|
|
511
|
+
}
|
|
512
|
+
}
|
|
238
513
|
// --- Helpers ---
|
|
239
514
|
function formatConfigPreview(config) {
|
|
240
515
|
const lines = [];
|
|
241
516
|
if (config.discovery) {
|
|
242
517
|
lines.push(`discovery: ${config.discovery}`);
|
|
243
518
|
}
|
|
244
|
-
if (Array.isArray(config.bootstrapList) &&
|
|
519
|
+
if (Array.isArray(config.bootstrapList) &&
|
|
520
|
+
config.bootstrapList.length > 0) {
|
|
245
521
|
lines.push(`bootstrapList: ${config.bootstrapList.length} 个节点`);
|
|
246
522
|
for (const addr of config.bootstrapList) {
|
|
247
523
|
lines.push(` ${addr}`);
|
|
248
524
|
}
|
|
249
525
|
}
|
|
250
|
-
if (Array.isArray(config.inboundTargets) &&
|
|
526
|
+
if (Array.isArray(config.inboundTargets) &&
|
|
527
|
+
config.inboundTargets.length > 0) {
|
|
251
528
|
lines.push("inboundTargets:");
|
|
252
529
|
for (const t of config.inboundTargets) {
|
|
253
530
|
lines.push(` - ${t.channel} / ${t.target}`);
|
|
@@ -257,13 +534,15 @@ function formatConfigPreview(config) {
|
|
|
257
534
|
lines.push(`inboundChannel: ${config.inboundChannel}`);
|
|
258
535
|
lines.push(`inboundTarget: ${config.inboundTarget}`);
|
|
259
536
|
}
|
|
260
|
-
if (Array.isArray(config.listenAddrs) &&
|
|
537
|
+
if (Array.isArray(config.listenAddrs) &&
|
|
538
|
+
config.listenAddrs.length > 0) {
|
|
261
539
|
lines.push(`listenAddrs: ${config.listenAddrs.join(", ")}`);
|
|
262
540
|
}
|
|
263
541
|
if (config.enableNATTraversal !== undefined) {
|
|
264
542
|
lines.push(`NAT 穿透: ${config.enableNATTraversal ? "开启" : "关闭"}`);
|
|
265
543
|
}
|
|
266
|
-
if (Array.isArray(config.relayList) &&
|
|
544
|
+
if (Array.isArray(config.relayList) &&
|
|
545
|
+
config.relayList.length > 0) {
|
|
267
546
|
lines.push(`relayList: ${config.relayList.length} 个节点`);
|
|
268
547
|
}
|
|
269
548
|
if (config.instanceName) {
|