libp2p-mesh 2026.6.8 → 2026.6.10
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/config-io.d.ts +1 -0
- package/dist/src/config-io.js +124 -21
- package/dist/src/wizard.js +0 -4
- package/package.json +1 -1
- package/src/config-io.ts +141 -25
- package/src/wizard.ts +0 -4
package/dist/src/config-io.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare const MULTIADDR_PATTERN: RegExp;
|
|
2
2
|
export declare function getDefaultConfig(): Record<string, unknown>;
|
|
3
|
+
export declare function validatePluginConfig(pluginConfig: Record<string, unknown>): void;
|
|
3
4
|
export declare function resolveConfigPath(): string;
|
|
4
5
|
export declare function readFullConfig(configPath: string): {
|
|
5
6
|
config: Record<string, unknown>;
|
package/dist/src/config-io.js
CHANGED
|
@@ -22,6 +22,107 @@ export function getDefaultConfig() {
|
|
|
22
22
|
deliveryAckTimeoutMs: 15000,
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
+
const PLUGIN_CONFIG_SCHEMA = {
|
|
26
|
+
type: "object",
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
properties: {
|
|
29
|
+
listenAddrs: { type: "array", items: { type: "string" } },
|
|
30
|
+
enableWebSocket: { type: "boolean" },
|
|
31
|
+
discovery: { type: "string", enum: ["mdns", "bootstrap", "dht"] },
|
|
32
|
+
bootstrapList: { type: "array", items: { type: "string" } },
|
|
33
|
+
meshTopic: { type: "string" },
|
|
34
|
+
enablePubsub: { type: "boolean" },
|
|
35
|
+
enableAgentSync: { type: "boolean" },
|
|
36
|
+
enableDHT: { type: "boolean" },
|
|
37
|
+
instanceName: { type: "string" },
|
|
38
|
+
enableNATTraversal: { type: "boolean" },
|
|
39
|
+
enableIdentify: { type: "boolean" },
|
|
40
|
+
enableAutoNAT: { type: "boolean" },
|
|
41
|
+
enableUPnP: { type: "boolean" },
|
|
42
|
+
enableCircuitRelay: { type: "boolean" },
|
|
43
|
+
enableCircuitRelayServer: { type: "boolean" },
|
|
44
|
+
enableDCUtR: { type: "boolean" },
|
|
45
|
+
relayList: { type: "array", items: { type: "string" } },
|
|
46
|
+
relayChannel: { type: "string" },
|
|
47
|
+
relayAccountId: { type: "string" },
|
|
48
|
+
discoverRelays: { type: "number" },
|
|
49
|
+
announceAddrs: { type: "array", items: { type: "string" } },
|
|
50
|
+
inboundChannel: { type: "string" },
|
|
51
|
+
inboundTarget: { type: "string" },
|
|
52
|
+
inboundTargets: {
|
|
53
|
+
type: "array",
|
|
54
|
+
items: {
|
|
55
|
+
type: "object",
|
|
56
|
+
additionalProperties: false,
|
|
57
|
+
properties: {
|
|
58
|
+
id: { type: "string" },
|
|
59
|
+
channel: { type: "string" },
|
|
60
|
+
target: { type: "string" },
|
|
61
|
+
},
|
|
62
|
+
required: ["channel", "target"],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
deliveryAckTimeoutMs: { type: "number" },
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
function validateSchemaValue(value, schema, pathName) {
|
|
69
|
+
const errors = [];
|
|
70
|
+
if (value === undefined)
|
|
71
|
+
return errors;
|
|
72
|
+
if (schema.type === "object") {
|
|
73
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
74
|
+
return [`${pathName}: expected object`];
|
|
75
|
+
}
|
|
76
|
+
const obj = value;
|
|
77
|
+
const properties = schema.properties ?? {};
|
|
78
|
+
for (const requiredKey of schema.required ?? []) {
|
|
79
|
+
if (obj[requiredKey] === undefined) {
|
|
80
|
+
errors.push(`${pathName}.${requiredKey}: required property is missing`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (schema.additionalProperties === false) {
|
|
84
|
+
for (const key of Object.keys(obj)) {
|
|
85
|
+
if (!properties[key]) {
|
|
86
|
+
errors.push(`${pathName}: must not have additional property "${key}"`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (const [key, childSchema] of Object.entries(properties)) {
|
|
91
|
+
errors.push(...validateSchemaValue(obj[key], childSchema, `${pathName}.${key}`));
|
|
92
|
+
}
|
|
93
|
+
return errors;
|
|
94
|
+
}
|
|
95
|
+
if (schema.type === "array") {
|
|
96
|
+
if (!Array.isArray(value)) {
|
|
97
|
+
return [`${pathName}: expected array`];
|
|
98
|
+
}
|
|
99
|
+
if (schema.items) {
|
|
100
|
+
value.forEach((item, index) => {
|
|
101
|
+
errors.push(...validateSchemaValue(item, schema.items, `${pathName}[${index}]`));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return errors;
|
|
105
|
+
}
|
|
106
|
+
if (schema.type === "string" && typeof value !== "string") {
|
|
107
|
+
return [`${pathName}: expected string`];
|
|
108
|
+
}
|
|
109
|
+
if (schema.type === "boolean" && typeof value !== "boolean") {
|
|
110
|
+
return [`${pathName}: expected boolean`];
|
|
111
|
+
}
|
|
112
|
+
if (schema.type === "number" && (typeof value !== "number" || Number.isNaN(value))) {
|
|
113
|
+
return [`${pathName}: expected number`];
|
|
114
|
+
}
|
|
115
|
+
if (schema.enum && typeof value === "string" && !schema.enum.includes(value)) {
|
|
116
|
+
errors.push(`${pathName}: must be one of ${schema.enum.join(", ")}`);
|
|
117
|
+
}
|
|
118
|
+
return errors;
|
|
119
|
+
}
|
|
120
|
+
export function validatePluginConfig(pluginConfig) {
|
|
121
|
+
const errors = validateSchemaValue(pluginConfig, PLUGIN_CONFIG_SCHEMA, "plugins.entries.libp2p-mesh.config");
|
|
122
|
+
if (errors.length > 0) {
|
|
123
|
+
throw new Error(`libp2p-mesh 配置无效:\n - ${errors.join("\n - ")}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
25
126
|
export function resolveConfigPath() {
|
|
26
127
|
if (process.env.OPENCLAW_CONFIG_PATH) {
|
|
27
128
|
const resolved = process.env.OPENCLAW_CONFIG_PATH.replace(/^~(?=$|\/|\\)/, os.homedir());
|
|
@@ -72,15 +173,6 @@ export function writeFullConfig(configPath, pluginConfigUpdates) {
|
|
|
72
173
|
throw new Error(`无法读取 ${configPath}: ${err.message}`);
|
|
73
174
|
}
|
|
74
175
|
}
|
|
75
|
-
// Create backup
|
|
76
|
-
try {
|
|
77
|
-
if (fs.existsSync(configPath)) {
|
|
78
|
-
fs.copyFileSync(configPath, configPath + ".bak");
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
console.warn("备份 openclaw.json 失败,继续写入。");
|
|
83
|
-
}
|
|
84
176
|
// Build output object with deep merge
|
|
85
177
|
const output = structuredClone(typeof base === "object" && !Array.isArray(base) ? base : {});
|
|
86
178
|
// Ensure plugins.entries["libp2p-mesh"] exists
|
|
@@ -105,20 +197,31 @@ export function writeFullConfig(configPath, pluginConfigUpdates) {
|
|
|
105
197
|
}
|
|
106
198
|
const existing = meshEntry.config;
|
|
107
199
|
meshEntry.config = { ...existing, ...pluginConfigUpdates };
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
typeof output.channels !== "object" ||
|
|
111
|
-
Array.isArray(output.channels)) {
|
|
112
|
-
output.channels = {};
|
|
113
|
-
}
|
|
200
|
+
// Older wizard versions wrote channels["libp2p-mesh"].enabled, but the
|
|
201
|
+
// channel schema does not allow that field. Clean it up when saving.
|
|
114
202
|
const channels = output.channels;
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
203
|
+
if (channels && typeof channels === "object" && !Array.isArray(channels)) {
|
|
204
|
+
const channelMap = channels;
|
|
205
|
+
const meshChannel = channelMap["libp2p-mesh"];
|
|
206
|
+
if (meshChannel &&
|
|
207
|
+
typeof meshChannel === "object" &&
|
|
208
|
+
!Array.isArray(meshChannel)) {
|
|
209
|
+
delete meshChannel.enabled;
|
|
210
|
+
if (Object.keys(meshChannel).length === 0) {
|
|
211
|
+
delete channelMap["libp2p-mesh"];
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
validatePluginConfig(meshEntry.config);
|
|
216
|
+
// Create backup after validation so invalid config never mutates files.
|
|
217
|
+
try {
|
|
218
|
+
if (fs.existsSync(configPath)) {
|
|
219
|
+
fs.copyFileSync(configPath, configPath + ".bak");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
console.warn("备份 openclaw.json 失败,继续写入。");
|
|
119
224
|
}
|
|
120
|
-
const meshChannel = channels["libp2p-mesh"];
|
|
121
|
-
meshChannel.enabled = true;
|
|
122
225
|
// Write atomically (write to temp, then rename)
|
|
123
226
|
const tmpPath = configPath + ".tmp";
|
|
124
227
|
try {
|
package/dist/src/wizard.js
CHANGED
|
@@ -95,7 +95,6 @@ function interactiveSelect(prompt, choices) {
|
|
|
95
95
|
return new Promise((resolve) => {
|
|
96
96
|
const wasRaw = process.stdin.isRaw;
|
|
97
97
|
const wasPaused = process.stdin.isPaused();
|
|
98
|
-
let resolved = false;
|
|
99
98
|
const onKeypress = (_str, key) => {
|
|
100
99
|
if (!key || !key.name)
|
|
101
100
|
return;
|
|
@@ -109,7 +108,6 @@ function interactiveSelect(prompt, choices) {
|
|
|
109
108
|
reRenderChoices(choices, selectedIdx);
|
|
110
109
|
}
|
|
111
110
|
else if (key.name === "return" || key.name === "space") {
|
|
112
|
-
resolved = true;
|
|
113
111
|
const chosen = choices[selectedIdx];
|
|
114
112
|
cleanup();
|
|
115
113
|
eraseChoices(choices.length);
|
|
@@ -123,8 +121,6 @@ function interactiveSelect(prompt, choices) {
|
|
|
123
121
|
}
|
|
124
122
|
};
|
|
125
123
|
const cleanup = () => {
|
|
126
|
-
if (resolved)
|
|
127
|
-
return;
|
|
128
124
|
try {
|
|
129
125
|
// Always restore to non-raw so subsequent readline works
|
|
130
126
|
process.stdin.setRawMode(false);
|
package/package.json
CHANGED
package/src/config-io.ts
CHANGED
|
@@ -25,6 +25,121 @@ export function getDefaultConfig(): Record<string, unknown> {
|
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
type JsonSchema = {
|
|
29
|
+
type?: "object" | "array" | "string" | "boolean" | "number";
|
|
30
|
+
enum?: string[];
|
|
31
|
+
additionalProperties?: boolean;
|
|
32
|
+
properties?: Record<string, JsonSchema>;
|
|
33
|
+
items?: JsonSchema;
|
|
34
|
+
required?: string[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const PLUGIN_CONFIG_SCHEMA: JsonSchema = {
|
|
38
|
+
type: "object",
|
|
39
|
+
additionalProperties: false,
|
|
40
|
+
properties: {
|
|
41
|
+
listenAddrs: { type: "array", items: { type: "string" } },
|
|
42
|
+
enableWebSocket: { type: "boolean" },
|
|
43
|
+
discovery: { type: "string", enum: ["mdns", "bootstrap", "dht"] },
|
|
44
|
+
bootstrapList: { type: "array", items: { type: "string" } },
|
|
45
|
+
meshTopic: { type: "string" },
|
|
46
|
+
enablePubsub: { type: "boolean" },
|
|
47
|
+
enableAgentSync: { type: "boolean" },
|
|
48
|
+
enableDHT: { type: "boolean" },
|
|
49
|
+
instanceName: { type: "string" },
|
|
50
|
+
enableNATTraversal: { type: "boolean" },
|
|
51
|
+
enableIdentify: { type: "boolean" },
|
|
52
|
+
enableAutoNAT: { type: "boolean" },
|
|
53
|
+
enableUPnP: { type: "boolean" },
|
|
54
|
+
enableCircuitRelay: { type: "boolean" },
|
|
55
|
+
enableCircuitRelayServer: { type: "boolean" },
|
|
56
|
+
enableDCUtR: { type: "boolean" },
|
|
57
|
+
relayList: { type: "array", items: { type: "string" } },
|
|
58
|
+
relayChannel: { type: "string" },
|
|
59
|
+
relayAccountId: { type: "string" },
|
|
60
|
+
discoverRelays: { type: "number" },
|
|
61
|
+
announceAddrs: { type: "array", items: { type: "string" } },
|
|
62
|
+
inboundChannel: { type: "string" },
|
|
63
|
+
inboundTarget: { type: "string" },
|
|
64
|
+
inboundTargets: {
|
|
65
|
+
type: "array",
|
|
66
|
+
items: {
|
|
67
|
+
type: "object",
|
|
68
|
+
additionalProperties: false,
|
|
69
|
+
properties: {
|
|
70
|
+
id: { type: "string" },
|
|
71
|
+
channel: { type: "string" },
|
|
72
|
+
target: { type: "string" },
|
|
73
|
+
},
|
|
74
|
+
required: ["channel", "target"],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
deliveryAckTimeoutMs: { type: "number" },
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
function validateSchemaValue(value: unknown, schema: JsonSchema, pathName: string): string[] {
|
|
82
|
+
const errors: string[] = [];
|
|
83
|
+
if (value === undefined) return errors;
|
|
84
|
+
|
|
85
|
+
if (schema.type === "object") {
|
|
86
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
87
|
+
return [`${pathName}: expected object`];
|
|
88
|
+
}
|
|
89
|
+
const obj = value as Record<string, unknown>;
|
|
90
|
+
const properties = schema.properties ?? {};
|
|
91
|
+
for (const requiredKey of schema.required ?? []) {
|
|
92
|
+
if (obj[requiredKey] === undefined) {
|
|
93
|
+
errors.push(`${pathName}.${requiredKey}: required property is missing`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (schema.additionalProperties === false) {
|
|
97
|
+
for (const key of Object.keys(obj)) {
|
|
98
|
+
if (!properties[key]) {
|
|
99
|
+
errors.push(`${pathName}: must not have additional property "${key}"`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
for (const [key, childSchema] of Object.entries(properties)) {
|
|
104
|
+
errors.push(...validateSchemaValue(obj[key], childSchema, `${pathName}.${key}`));
|
|
105
|
+
}
|
|
106
|
+
return errors;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (schema.type === "array") {
|
|
110
|
+
if (!Array.isArray(value)) {
|
|
111
|
+
return [`${pathName}: expected array`];
|
|
112
|
+
}
|
|
113
|
+
if (schema.items) {
|
|
114
|
+
value.forEach((item, index) => {
|
|
115
|
+
errors.push(...validateSchemaValue(item, schema.items!, `${pathName}[${index}]`));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return errors;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (schema.type === "string" && typeof value !== "string") {
|
|
122
|
+
return [`${pathName}: expected string`];
|
|
123
|
+
}
|
|
124
|
+
if (schema.type === "boolean" && typeof value !== "boolean") {
|
|
125
|
+
return [`${pathName}: expected boolean`];
|
|
126
|
+
}
|
|
127
|
+
if (schema.type === "number" && (typeof value !== "number" || Number.isNaN(value))) {
|
|
128
|
+
return [`${pathName}: expected number`];
|
|
129
|
+
}
|
|
130
|
+
if (schema.enum && typeof value === "string" && !schema.enum.includes(value)) {
|
|
131
|
+
errors.push(`${pathName}: must be one of ${schema.enum.join(", ")}`);
|
|
132
|
+
}
|
|
133
|
+
return errors;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function validatePluginConfig(pluginConfig: Record<string, unknown>): void {
|
|
137
|
+
const errors = validateSchemaValue(pluginConfig, PLUGIN_CONFIG_SCHEMA, "plugins.entries.libp2p-mesh.config");
|
|
138
|
+
if (errors.length > 0) {
|
|
139
|
+
throw new Error(`libp2p-mesh 配置无效:\n - ${errors.join("\n - ")}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
28
143
|
export function resolveConfigPath(): string {
|
|
29
144
|
if (process.env.OPENCLAW_CONFIG_PATH) {
|
|
30
145
|
const resolved = process.env.OPENCLAW_CONFIG_PATH.replace(/^~(?=$|\/|\\)/, os.homedir());
|
|
@@ -92,15 +207,6 @@ export function writeFullConfig(
|
|
|
92
207
|
}
|
|
93
208
|
}
|
|
94
209
|
|
|
95
|
-
// Create backup
|
|
96
|
-
try {
|
|
97
|
-
if (fs.existsSync(configPath)) {
|
|
98
|
-
fs.copyFileSync(configPath, configPath + ".bak");
|
|
99
|
-
}
|
|
100
|
-
} catch {
|
|
101
|
-
console.warn("备份 openclaw.json 失败,继续写入。");
|
|
102
|
-
}
|
|
103
|
-
|
|
104
210
|
// Build output object with deep merge
|
|
105
211
|
const output = structuredClone(
|
|
106
212
|
typeof base === "object" && !Array.isArray(base) ? base : {},
|
|
@@ -132,24 +238,34 @@ export function writeFullConfig(
|
|
|
132
238
|
const existing = meshEntry.config as Record<string, unknown>;
|
|
133
239
|
meshEntry.config = { ...existing, ...pluginConfigUpdates };
|
|
134
240
|
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
241
|
+
// Older wizard versions wrote channels["libp2p-mesh"].enabled, but the
|
|
242
|
+
// channel schema does not allow that field. Clean it up when saving.
|
|
243
|
+
const channels = output.channels;
|
|
244
|
+
if (channels && typeof channels === "object" && !Array.isArray(channels)) {
|
|
245
|
+
const channelMap = channels as Record<string, unknown>;
|
|
246
|
+
const meshChannel = channelMap["libp2p-mesh"];
|
|
247
|
+
if (
|
|
248
|
+
meshChannel &&
|
|
249
|
+
typeof meshChannel === "object" &&
|
|
250
|
+
!Array.isArray(meshChannel)
|
|
251
|
+
) {
|
|
252
|
+
delete (meshChannel as Record<string, unknown>).enabled;
|
|
253
|
+
if (Object.keys(meshChannel).length === 0) {
|
|
254
|
+
delete channelMap["libp2p-mesh"];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
142
257
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
258
|
+
|
|
259
|
+
validatePluginConfig(meshEntry.config as Record<string, unknown>);
|
|
260
|
+
|
|
261
|
+
// Create backup after validation so invalid config never mutates files.
|
|
262
|
+
try {
|
|
263
|
+
if (fs.existsSync(configPath)) {
|
|
264
|
+
fs.copyFileSync(configPath, configPath + ".bak");
|
|
265
|
+
}
|
|
266
|
+
} catch {
|
|
267
|
+
console.warn("备份 openclaw.json 失败,继续写入。");
|
|
150
268
|
}
|
|
151
|
-
const meshChannel = channels["libp2p-mesh"] as Record<string, unknown>;
|
|
152
|
-
meshChannel.enabled = true;
|
|
153
269
|
|
|
154
270
|
// Write atomically (write to temp, then rename)
|
|
155
271
|
const tmpPath = configPath + ".tmp";
|
package/src/wizard.ts
CHANGED
|
@@ -149,8 +149,6 @@ function interactiveSelect(
|
|
|
149
149
|
const wasRaw = process.stdin.isRaw;
|
|
150
150
|
const wasPaused = process.stdin.isPaused();
|
|
151
151
|
|
|
152
|
-
let resolved = false;
|
|
153
|
-
|
|
154
152
|
const onKeypress = (
|
|
155
153
|
_str: string | undefined,
|
|
156
154
|
key: { name?: string; ctrl?: boolean },
|
|
@@ -165,7 +163,6 @@ function interactiveSelect(
|
|
|
165
163
|
selectedIdx = (selectedIdx + 1) % choices.length;
|
|
166
164
|
reRenderChoices(choices, selectedIdx);
|
|
167
165
|
} else if (key.name === "return" || key.name === "space") {
|
|
168
|
-
resolved = true;
|
|
169
166
|
const chosen = choices[selectedIdx]!;
|
|
170
167
|
cleanup();
|
|
171
168
|
eraseChoices(choices.length);
|
|
@@ -179,7 +176,6 @@ function interactiveSelect(
|
|
|
179
176
|
};
|
|
180
177
|
|
|
181
178
|
const cleanup = () => {
|
|
182
|
-
if (resolved) return;
|
|
183
179
|
try {
|
|
184
180
|
// Always restore to non-raw so subsequent readline works
|
|
185
181
|
process.stdin.setRawMode(false);
|