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/src/cli.ts ADDED
@@ -0,0 +1,226 @@
1
+ import type { PluginLogger } from "openclaw/plugin-sdk/core";
2
+ import {
3
+ resolveConfigPath,
4
+ readFullConfig,
5
+ writeFullConfig,
6
+ getNonDefaultConfig,
7
+ getDefaultConfig,
8
+ } from "./config-io.js";
9
+ import {
10
+ createReadlinePrompter,
11
+ runSetupWizard,
12
+ WizardCancelledError,
13
+ } from "./wizard.js";
14
+
15
+ // OpenClawPluginCliContext is defined in openclaw's types.d.ts but not re-exported
16
+ // from the public SDK barrel modules at this version. Define the shape locally.
17
+ interface MinimalCommand {
18
+ command(name: string): MinimalCommand;
19
+ description(desc: string): MinimalCommand;
20
+ option(flags: string, description?: string): MinimalCommand;
21
+ action(fn: (...args: any[]) => void | Promise<void>): MinimalCommand;
22
+ }
23
+ interface OpenClawPluginCliContext {
24
+ program: MinimalCommand;
25
+ config: Record<string, unknown>;
26
+ workspaceDir?: string;
27
+ logger: PluginLogger;
28
+ }
29
+
30
+ export function registerLibp2pMeshCli(ctx: OpenClawPluginCliContext): void {
31
+ const { program, config: openclawConfig } = ctx;
32
+
33
+ const meshCmd = program
34
+ .command("libp2p-mesh")
35
+ .description("P2P Mesh 网络插件配置管理");
36
+
37
+ // ---- setup ----
38
+ meshCmd
39
+ .command("setup")
40
+ .description("交互式配置向导")
41
+ .action(async () => {
42
+ const configPath = resolveConfigPath();
43
+ const { pluginConfig } = readFullConfig(configPath);
44
+
45
+ // Discover available chat channels from config
46
+ const channels = openclawConfig.channels;
47
+ const availableChannels: string[] = [];
48
+ if (channels && typeof channels === "object" && !Array.isArray(channels)) {
49
+ for (const [id, entry] of Object.entries(channels as Record<string, unknown>)) {
50
+ if (
51
+ id !== "libp2p-mesh" &&
52
+ entry &&
53
+ typeof entry === "object" &&
54
+ (entry as Record<string, unknown>).enabled !== false
55
+ ) {
56
+ availableChannels.push(id);
57
+ }
58
+ }
59
+ }
60
+
61
+ const prompter = createReadlinePrompter();
62
+ try {
63
+ const newConfig = await runSetupWizard(prompter, pluginConfig, availableChannels);
64
+ writeFullConfig(configPath, newConfig);
65
+ console.log(`\n✓ 配置已写入 ${configPath}`);
66
+ console.log(" 运行 openclaw gateway restart 使新配置生效。");
67
+ } catch (err) {
68
+ if (err instanceof WizardCancelledError) {
69
+ process.exit(0);
70
+ }
71
+ if (err instanceof Error) {
72
+ console.error(`\n✗ ${err.message}`);
73
+ }
74
+ process.exit(1);
75
+ } finally {
76
+ prompter.close();
77
+ }
78
+ });
79
+
80
+ // ---- config subcommand ----
81
+ const configCmd = meshCmd
82
+ .command("config")
83
+ .description("增量配置管理");
84
+
85
+ // ---- config list ----
86
+ configCmd
87
+ .command("list")
88
+ .description("列出当前所有非默认配置")
89
+ .action(() => {
90
+ const configPath = resolveConfigPath();
91
+ const { pluginConfig } = readFullConfig(configPath);
92
+ const nonDefault = getNonDefaultConfig(pluginConfig);
93
+ const keys = Object.keys(nonDefault);
94
+
95
+ if (keys.length === 0) {
96
+ console.log("当前无自定义配置,全部使用默认值。");
97
+ console.log("运行 openclaw libp2p-mesh setup 进行配置。");
98
+ return;
99
+ }
100
+
101
+ console.log("─────────────────────────────────");
102
+ console.log(" 当前 libp2p-mesh 配置:\n");
103
+ for (const key of keys) {
104
+ const value = nonDefault[key];
105
+ if (Array.isArray(value)) {
106
+ console.log(` ${key}:`);
107
+ if (value.length === 0) {
108
+ console.log(" (空列表)");
109
+ } else {
110
+ for (const item of value) {
111
+ if (typeof item === "object" && item !== null) {
112
+ const obj = item as Record<string, unknown>;
113
+ const label = obj.id ? `${obj.id} — ` : "";
114
+ console.log(` - ${label}${obj.channel ?? ""} / ${obj.target ?? ""}`);
115
+ } else {
116
+ console.log(` - ${item}`);
117
+ }
118
+ }
119
+ }
120
+ } else {
121
+ console.log(` ${key}: ${value}`);
122
+ }
123
+ }
124
+ console.log(`\n ...共 ${keys.length} 项非默认配置`);
125
+ });
126
+
127
+ // ---- config get ----
128
+ configCmd
129
+ .command("get <key>")
130
+ .description("读取单个配置值")
131
+ .action((key: string) => {
132
+ const configPath = resolveConfigPath();
133
+ const { pluginConfig } = readFullConfig(configPath);
134
+ const defaults = getDefaultConfig();
135
+ const value = key in pluginConfig ? pluginConfig[key] : defaults[key];
136
+ if (value === undefined) {
137
+ console.log(` (未配置)`);
138
+ } else if (Array.isArray(value)) {
139
+ if (value.length === 0) {
140
+ console.log(" (空列表)");
141
+ } else {
142
+ for (const item of value) {
143
+ if (typeof item === "object" && item !== null) {
144
+ console.log(JSON.stringify(item, null, 2));
145
+ } else {
146
+ console.log(` ${item}`);
147
+ }
148
+ }
149
+ }
150
+ } else {
151
+ console.log(` ${value}`);
152
+ }
153
+ });
154
+
155
+ // ---- config set ----
156
+ configCmd
157
+ .command("set <key> [value]")
158
+ .description("设置配置值。数组类型使用 --add / --remove")
159
+ .option("--add <item>", "追加到数组")
160
+ .option("--remove <item>", "从数组中移除")
161
+ .action(async (key: string, value: string | undefined, opts: { add?: string; remove?: string }) => {
162
+ const configPath = resolveConfigPath();
163
+ const { pluginConfig } = readFullConfig(configPath);
164
+ const defaults = getDefaultConfig();
165
+ const current = key in pluginConfig ? pluginConfig[key] : defaults[key];
166
+ const oldValue = current;
167
+
168
+ let newValue: unknown;
169
+
170
+ // Array operations
171
+ if (opts.add || opts.remove) {
172
+ const arr: unknown[] = Array.isArray(current) ? [...current] : [];
173
+ if (opts.add) {
174
+ if (arr.includes(opts.add)) {
175
+ console.log(` ⚠ 该值已存在`);
176
+ return;
177
+ }
178
+ arr.push(opts.add);
179
+ console.log(` ✓ 已追加`);
180
+ }
181
+ if (opts.remove) {
182
+ const idx = arr.indexOf(opts.remove);
183
+ if (idx === -1) {
184
+ console.log(` ⚠ 未找到该值`);
185
+ return;
186
+ }
187
+ arr.splice(idx, 1);
188
+ console.log(` ✓ 已移除`);
189
+ }
190
+ newValue = arr;
191
+ } else {
192
+ // Scalar
193
+ if (value === undefined) {
194
+ console.error(" 请提供值。用法: openclaw libp2p-mesh config set <key> <value>");
195
+ process.exit(1);
196
+ }
197
+ // Auto-detect boolean and number
198
+ if (value === "true") newValue = true;
199
+ else if (value === "false") newValue = false;
200
+ else if (/^-?\d+(\.\d+)?$/.test(value)) newValue = Number(value);
201
+ else newValue = value;
202
+ }
203
+
204
+ // Write
205
+ const updates = { ...pluginConfig, [key]: newValue };
206
+ writeFullConfig(configPath, updates);
207
+
208
+ // Feedback
209
+ if (!opts.add && !opts.remove) {
210
+ console.log(` ✓ ${key}: ${JSON.stringify(oldValue)} → ${JSON.stringify(newValue)}`);
211
+ }
212
+ });
213
+
214
+ // ---- config unset ----
215
+ configCmd
216
+ .command("unset <key>")
217
+ .description("删除 key,恢复默认值")
218
+ .action((key: string) => {
219
+ const configPath = resolveConfigPath();
220
+ const { pluginConfig } = readFullConfig(configPath);
221
+ const newConfig = { ...pluginConfig };
222
+ delete newConfig[key];
223
+ writeFullConfig(configPath, newConfig);
224
+ console.log(` ✓ ${key} 已恢复为默认值`);
225
+ });
226
+ }
@@ -0,0 +1,204 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+
5
+ export const MULTIADDR_PATTERN = /^(\/ip[46]\/(?:[\d.]+|[0-9a-fA-F:]+)|\/dns(?:[46])?\/[a-zA-Z0-9][a-zA-Z0-9.-]*)\/(?:tcp|udp|ws|wss)\/\d+(?:\/p2p\/[12][a-zA-Z2-7]{48,})?$/;
6
+
7
+ export function getDefaultConfig(): Record<string, unknown> {
8
+ return {
9
+ listenAddrs: ["/ip4/0.0.0.0/tcp/0"],
10
+ enableWebSocket: false,
11
+ discovery: "mdns",
12
+ meshTopic: "openclaw-mesh",
13
+ enablePubsub: true,
14
+ enableAgentSync: true,
15
+ enableDHT: true,
16
+ enableNATTraversal: true,
17
+ enableIdentify: true,
18
+ enableAutoNAT: true,
19
+ enableUPnP: true,
20
+ enableCircuitRelay: true,
21
+ enableCircuitRelayServer: false,
22
+ enableDCUtR: true,
23
+ discoverRelays: 0,
24
+ deliveryAckTimeoutMs: 15000,
25
+ };
26
+ }
27
+
28
+ export function resolveConfigPath(): string {
29
+ if (process.env.OPENCLAW_CONFIG_PATH) {
30
+ const resolved = process.env.OPENCLAW_CONFIG_PATH.replace(/^~(?=$|\/|\\)/, os.homedir());
31
+ return path.resolve(resolved);
32
+ }
33
+ const stateDir = process.env.OPENCLAW_STATE_DIR
34
+ ? process.env.OPENCLAW_STATE_DIR.replace(/^~(?=$|\/|\\)/, os.homedir())
35
+ : path.join(os.homedir(), ".openclaw");
36
+ return path.join(stateDir, "openclaw.json");
37
+ }
38
+
39
+ export function readFullConfig(configPath: string): {
40
+ config: Record<string, unknown>;
41
+ pluginConfig: Record<string, unknown>;
42
+ } {
43
+ let raw: Record<string, unknown>;
44
+ try {
45
+ raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
46
+ } catch (err: unknown) {
47
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
48
+ return { config: {}, pluginConfig: {} };
49
+ }
50
+ throw new Error(
51
+ `无法解析 ${configPath}: ${(err as Error).message}\n请手动修复 JSON 格式后重试。`,
52
+ );
53
+ }
54
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
55
+ throw new Error(`${configPath} 内容不是合法的 JSON 对象。`);
56
+ }
57
+
58
+ const plugins = (raw as Record<string, unknown>).plugins;
59
+ const entries =
60
+ plugins && typeof plugins === "object" && !Array.isArray(plugins)
61
+ ? (plugins as Record<string, unknown>).entries
62
+ : undefined;
63
+ const entry =
64
+ entries && typeof entries === "object" && !Array.isArray(entries)
65
+ ? (entries as Record<string, unknown>)["libp2p-mesh"]
66
+ : undefined;
67
+ const pluginConfig =
68
+ entry && typeof entry === "object" && !Array.isArray(entry)
69
+ ? ((entry as Record<string, unknown>).config as Record<string, unknown>) ?? {}
70
+ : {};
71
+
72
+ return { config: raw as Record<string, unknown>, pluginConfig };
73
+ }
74
+
75
+ export function writeFullConfig(
76
+ configPath: string,
77
+ pluginConfigUpdates: Record<string, unknown>,
78
+ ): void {
79
+ // Ensure directory exists
80
+ const dir = path.dirname(configPath);
81
+ fs.mkdirSync(dir, { recursive: true });
82
+
83
+ // Read existing or start fresh
84
+ let base: Record<string, unknown> = {};
85
+ try {
86
+ base = JSON.parse(fs.readFileSync(configPath, "utf-8"));
87
+ } catch (err: unknown) {
88
+ if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
89
+ throw new Error(
90
+ `无法读取 ${configPath}: ${(err as Error).message}`,
91
+ );
92
+ }
93
+ }
94
+
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
+ // Build output object with deep merge
105
+ const output = structuredClone(
106
+ typeof base === "object" && !Array.isArray(base) ? base : {},
107
+ );
108
+
109
+ // Ensure plugins.entries["libp2p-mesh"] exists
110
+ if (!output.plugins || typeof output.plugins !== "object" || Array.isArray(output.plugins)) {
111
+ output.plugins = {};
112
+ }
113
+ const plugins = output.plugins as Record<string, unknown>;
114
+ if (!plugins.entries || typeof plugins.entries !== "object" || Array.isArray(plugins.entries)) {
115
+ plugins.entries = {};
116
+ }
117
+ const entries = plugins.entries as Record<string, unknown>;
118
+ if (
119
+ !entries["libp2p-mesh"] ||
120
+ typeof entries["libp2p-mesh"] !== "object" ||
121
+ Array.isArray(entries["libp2p-mesh"])
122
+ ) {
123
+ entries["libp2p-mesh"] = {};
124
+ }
125
+ const meshEntry = entries["libp2p-mesh"] as Record<string, unknown>;
126
+ meshEntry.enabled = true;
127
+
128
+ // Merge plugin config shallowly (key-level)
129
+ if (!meshEntry.config || typeof meshEntry.config !== "object" || Array.isArray(meshEntry.config)) {
130
+ meshEntry.config = {};
131
+ }
132
+ const existing = meshEntry.config as Record<string, unknown>;
133
+ meshEntry.config = { ...existing, ...pluginConfigUpdates };
134
+
135
+ // Ensure channels["libp2p-mesh"].enabled exists
136
+ if (
137
+ !output.channels ||
138
+ typeof output.channels !== "object" ||
139
+ Array.isArray(output.channels)
140
+ ) {
141
+ output.channels = {};
142
+ }
143
+ const channels = output.channels as Record<string, unknown>;
144
+ if (
145
+ !channels["libp2p-mesh"] ||
146
+ typeof channels["libp2p-mesh"] !== "object" ||
147
+ Array.isArray(channels["libp2p-mesh"])
148
+ ) {
149
+ channels["libp2p-mesh"] = {};
150
+ }
151
+ const meshChannel = channels["libp2p-mesh"] as Record<string, unknown>;
152
+ meshChannel.enabled = true;
153
+
154
+ // Write atomically (write to temp, then rename)
155
+ const tmpPath = configPath + ".tmp";
156
+ try {
157
+ fs.writeFileSync(tmpPath, JSON.stringify(output, null, 2) + "\n", "utf-8");
158
+ fs.renameSync(tmpPath, configPath);
159
+ } catch (err: unknown) {
160
+ // Rollback from backup
161
+ try {
162
+ if (fs.existsSync(configPath + ".bak")) {
163
+ fs.copyFileSync(configPath + ".bak", configPath);
164
+ }
165
+ } catch {
166
+ // rollback failure — leave existing state
167
+ }
168
+ // Clean up temp
169
+ try {
170
+ fs.unlinkSync(tmpPath);
171
+ } catch {
172
+ // ok
173
+ }
174
+ throw new Error(
175
+ `写入 ${configPath} 失败:${(err as Error).message}。配置未更改。`,
176
+ );
177
+ }
178
+ }
179
+
180
+ export function getNonDefaultConfig(
181
+ pluginConfig: Record<string, unknown>,
182
+ ): Record<string, unknown> {
183
+ const defaults = getDefaultConfig();
184
+ const result: Record<string, unknown> = {};
185
+ for (const key of Object.keys(pluginConfig)) {
186
+ const value = pluginConfig[key];
187
+ if (value === undefined) continue;
188
+ if (!(key in defaults)) {
189
+ // Unknown key — include (could be a new key added in later version)
190
+ result[key] = value;
191
+ continue;
192
+ }
193
+ const def = defaults[key];
194
+ if (Array.isArray(value) && Array.isArray(def)) {
195
+ if (value.length === 0 && def.length > 0) continue;
196
+ if (value.length !== def.length || value.some((v, i) => v !== def[i])) {
197
+ result[key] = value;
198
+ }
199
+ } else if (value !== def) {
200
+ result[key] = value;
201
+ }
202
+ }
203
+ return result;
204
+ }
@@ -1,4 +1,7 @@
1
- import { spawn } from "node:child_process";
1
+ import type {
2
+ ChannelOutboundAdapter,
3
+ OpenClawConfig,
4
+ } from "openclaw/plugin-sdk/core";
2
5
  import type {
3
6
  InboundDeliveryAdapter,
4
7
  InboundDeliveryRequest,
@@ -11,107 +14,60 @@ export type DeliveryLogger = {
11
14
  warn?: (message: string) => void;
12
15
  };
13
16
 
14
- export function createOpenClawCliInboundDelivery(options?: {
15
- command?: string;
16
- timeoutMs?: number;
17
+ export type LoadChannelOutboundAdapter = (
18
+ channel: string,
19
+ ) => Promise<ChannelOutboundAdapter | undefined>;
20
+
21
+ function summarizeError(error: unknown): string {
22
+ return error instanceof Error ? error.message : String(error);
23
+ }
24
+
25
+ export function createOpenClawRuntimeInboundDelivery(options: {
26
+ config: OpenClawConfig;
27
+ loadAdapter: LoadChannelOutboundAdapter;
17
28
  logger?: DeliveryLogger;
18
29
  }): InboundDeliveryAdapter {
19
- const command = options?.command ?? "openclaw";
20
- const timeoutMs = options?.timeoutMs ?? 15000;
21
- const logger = options?.logger;
30
+ const { config, loadAdapter, logger } = options;
22
31
 
23
32
  return {
24
- deliver(request: InboundDeliveryRequest): Promise<InboundDeliveryResult> {
25
- const args = [
26
- "message",
27
- "send",
28
- "--channel",
29
- request.channel,
30
- "--target",
31
- request.target,
32
- "--message",
33
- request.text,
34
- ];
35
-
33
+ async deliver(request: InboundDeliveryRequest): Promise<InboundDeliveryResult> {
36
34
  logger?.debug?.(
37
- `[libp2p-mesh] Forwarding inbound delivery via CLI: ${command} ${args.join(" ")}`,
35
+ `[libp2p-mesh] Forwarding inbound delivery via runtime channel adapter: ${request.channel}/${request.target}`,
38
36
  );
39
37
 
40
- return new Promise((resolve) => {
41
- const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
42
- const stdout: Buffer[] = [];
43
- const stderr: Buffer[] = [];
44
- let settled = false;
45
- let timeout: ReturnType<typeof setTimeout> | undefined;
46
-
47
- const finish = (result: InboundDeliveryResult): void => {
48
- if (settled) {
49
- return;
50
- }
51
- settled = true;
52
- if (timeout) {
53
- clearTimeout(timeout);
54
- }
55
- resolve(result);
38
+ const adapter = await loadAdapter(request.channel);
39
+ if (!adapter?.sendText) {
40
+ return {
41
+ ok: false,
42
+ channel: request.channel,
43
+ target: request.target,
44
+ error: `channel ${request.channel} does not expose runtime text delivery`,
56
45
  };
46
+ }
57
47
 
58
- timeout = setTimeout(() => {
59
- logger?.warn?.(
60
- `[libp2p-mesh] Inbound delivery command timed out after ${timeoutMs}ms`,
61
- );
62
- child.kill("SIGTERM");
63
- finish({
64
- ok: false,
65
- channel: request.channel,
66
- target: request.target,
67
- error: `openclaw message send timed out after ${timeoutMs}ms`,
68
- });
69
- }, timeoutMs);
70
-
71
- child.stdout.on("data", (chunk: Buffer) => {
72
- stdout.push(chunk);
73
- });
74
-
75
- child.stderr.on("data", (chunk: Buffer) => {
76
- stderr.push(chunk);
77
- });
78
-
79
- child.on("error", (err) => {
80
- finish({
81
- ok: false,
82
- channel: request.channel,
83
- target: request.target,
84
- error: String(err),
85
- });
48
+ try {
49
+ await adapter.sendText({
50
+ cfg: config,
51
+ to: request.target,
52
+ text: request.text,
86
53
  });
54
+ } catch (error) {
55
+ return {
56
+ ok: false,
57
+ channel: request.channel,
58
+ target: request.target,
59
+ error: summarizeError(error),
60
+ };
61
+ }
87
62
 
88
- child.on("close", (code) => {
89
- if (code === 0) {
90
- logger?.info?.(
91
- `[libp2p-mesh] Delivered inbound message to ${request.channel}/${request.target}`,
92
- );
93
- finish({
94
- ok: true,
95
- channel: request.channel,
96
- target: request.target,
97
- });
98
- return;
99
- }
100
-
101
- const stderrText = Buffer.concat(stderr).toString().trim();
102
- const stdoutText = Buffer.concat(stdout).toString().trim();
103
-
104
- finish({
105
- ok: false,
106
- channel: request.channel,
107
- target: request.target,
108
- error:
109
- stderrText ||
110
- stdoutText ||
111
- `openclaw message send exited with code ${code}`,
112
- });
113
- });
114
- });
63
+ logger?.info?.(
64
+ `[libp2p-mesh] Delivered inbound message to ${request.channel}/${request.target}`,
65
+ );
66
+ return {
67
+ ok: true,
68
+ channel: request.channel,
69
+ target: request.target,
70
+ };
115
71
  },
116
72
  };
117
73
  }
package/src/inbound.ts CHANGED
@@ -8,10 +8,11 @@ export type InboundHandlerDeps = {
8
8
  warn?: (msg: string) => void;
9
9
  error?: (msg: string) => void;
10
10
  };
11
+ sendToChannel?: (channelId: string, target: string, text: string) => Promise<void>;
11
12
  };
12
13
 
13
14
  export function handleP2PInbound(msg: P2PMessage, deps: InboundHandlerDeps): void {
14
- const { logger } = deps;
15
+ const { logger, sendToChannel } = deps;
15
16
  const instanceTag = msg.instanceId ? ` [instance: ${msg.instanceId}]` : "";
16
17
  const signedTag = msg.signature ? " [signed]" : "";
17
18
 
@@ -45,9 +46,20 @@ export function handleP2PInbound(msg: P2PMessage, deps: InboundHandlerDeps): voi
45
46
  logger?.info?.(
46
47
  `[libp2p-mesh] Broadcast from ${msg.from}${instanceTag}${signedTag} on topic ${msg.topic ?? "(none)"}: ${msg.payload}`,
47
48
  );
48
- } else {
49
- logger?.info?.(
50
- `[libp2p-mesh] Direct message from ${msg.from}${instanceTag}${signedTag}: ${msg.payload}`,
51
- );
49
+ return;
50
+ }
51
+
52
+ // Direct message — log and forward to local channel
53
+ logger?.info?.(
54
+ `[libp2p-mesh] Direct message from ${msg.from}${instanceTag}${signedTag}: ${msg.payload}`,
55
+ );
56
+
57
+ if (!sendToChannel || !msg.payload) {
58
+ return;
52
59
  }
60
+
61
+ const text = `[来自 ${msg.from}]\n${msg.payload}`;
62
+ sendToChannel("libp2p-mesh", msg.from, text).catch((err) => {
63
+ logger?.error?.(`[libp2p-mesh] Failed to forward direct message from ${msg.from}: ${err}`);
64
+ });
53
65
  }